diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7606e5809a..35461dd30c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -460,6 +460,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
+
+
startAvatarSelection());
view.findViewById(R.id.mms_group_hint)
@@ -228,12 +188,14 @@ public class EditProfileFragment extends LoggingFragment {
view.findViewById(R.id.description_text).setVisibility(View.GONE);
view.findViewById(R.id.avatar_placeholder).setImageResource(R.drawable.ic_group_outline_40);
} else {
+ EditTextUtil.addGraphemeClusterLimitFilter(givenName, EditProfileNameFragment.NAME_MAX_GLYPHS);
+ EditTextUtil.addGraphemeClusterLimitFilter(familyName, EditProfileNameFragment.NAME_MAX_GLYPHS);
this.givenName.addTextChangedListener(new AfterTextChanged(s -> {
- trimInPlace(s);
+ EditProfileNameFragment.trimFieldToMaxByteLength(s);
viewModel.setGivenName(s.toString());
}));
this.familyName.addTextChangedListener(new AfterTextChanged(s -> {
- trimInPlace(s);
+ EditProfileNameFragment.trimFieldToMaxByteLength(s);
viewModel.setFamilyName(s.toString());
}));
LearnMoreTextView descriptionText = view.findViewById(R.id.description_text);
@@ -249,11 +211,6 @@ public class EditProfileFragment extends LoggingFragment {
this.finishButton.setText(arguments.getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
- this.usernameEditButton.setOnClickListener(v -> {
- NavDirections action = EditProfileFragmentDirections.actionEditUsername();
- Navigation.findNavController(v).navigate(action);
- });
-
if (arguments.getBoolean(SHOW_TOOLBAR, true)) {
this.toolbar.setVisibility(View.VISIBLE);
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
@@ -285,10 +242,6 @@ public class EditProfileFragment extends LoggingFragment {
});
}
- private void initializeUsername() {
- viewModel.username().observe(getViewLifecycleOwner(), this::onUsernameChanged);
- }
-
private static void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) {
String fieldTrimmed = field.getText().toString().trim();
String valueTrimmed = value.trim();
@@ -304,10 +257,6 @@ public class EditProfileFragment extends LoggingFragment {
}
}
- private void onUsernameChanged(@NonNull Optional username) {
- this.username.setText(username.transform(s -> "@" + s).or(""));
- }
-
private void startAvatarSelection() {
AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveProfilePhoto(),
true,
@@ -375,14 +324,6 @@ public class EditProfileFragment extends LoggingFragment {
animation.start();
}
- private static void trimInPlace(Editable s) {
- int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileName.MAX_PART_LENGTH).length();
-
- if (s.length() > trimmedLength) {
- s.delete(trimmedLength, s.length());
- }
- }
-
public interface Controller {
void onProfileNameUploadCompleted();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java
index ec0cb5fb78..53978025b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java
@@ -27,7 +27,6 @@ class EditProfileViewModel extends ViewModel {
private final LiveData internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
private final MutableLiveData internalAvatar = new MutableLiveData<>();
private final MutableLiveData originalAvatar = new MutableLiveData<>();
- private final MutableLiveData> internalUsername = new MutableLiveData<>();
private final MutableLiveData originalDisplayName = new MutableLiveData<>();
private final LiveData isFormValid;
private final EditProfileRepository repository;
@@ -77,10 +76,6 @@ class EditProfileViewModel extends ViewModel {
return Transformations.distinctUntilChanged(internalAvatar);
}
- public LiveData> username() {
- return internalUsername;
- }
-
public boolean hasAvatar() {
return internalAvatar.getValue() != null;
}
@@ -105,10 +100,6 @@ class EditProfileViewModel extends ViewModel {
internalAvatar.setValue(avatar);
}
- public void refreshUsername() {
- repository.getCurrentUsername(internalUsername::postValue);
- }
-
public void submitProfile(Consumer uploadResultConsumer) {
ProfileName profileName = isGroup() ? ProfileName.EMPTY : internalProfileName.getValue();
String displayName = isGroup() ? givenName.getValue() : "";
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java
index 3da341a25a..45f2f60a86 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java
@@ -29,7 +29,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
-class EditSelfProfileRepository implements EditProfileRepository {
+public class EditSelfProfileRepository implements EditProfileRepository {
private static final String TAG = Log.tag(EditSelfProfileRepository.class);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java
new file mode 100644
index 0000000000..63059a8f88
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/EditProfileNameFragment.java
@@ -0,0 +1,82 @@
+package org.thoughtcrime.securesms.profiles.manage;
+
+import android.os.Bundle;
+import android.text.Editable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.Navigation;
+
+import org.signal.core.util.EditTextUtil;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
+import org.thoughtcrime.securesms.profiles.ProfileName;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.StringUtil;
+import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.thoughtcrime.securesms.util.text.AfterTextChanged;
+
+/**
+ * Simple fragment to edit your profile name.
+ */
+public class EditProfileNameFragment extends Fragment {
+
+ public static final int NAME_MAX_GLYPHS = 26;
+
+ private EditText givenName;
+ private EditText familyName;
+
+ @Override
+ public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.edit_profile_name_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ this.givenName = view.findViewById(R.id.edit_profile_name_given_name);
+ this.familyName = view.findViewById(R.id.edit_profile_name_family_name);
+
+ this.givenName.setText(Recipient.self().getProfileName().getGivenName());
+ this.familyName.setText(Recipient.self().getProfileName().getFamilyName());
+
+ view.findViewById(R.id.toolbar)
+ .setNavigationOnClickListener(v -> Navigation.findNavController(view)
+ .popBackStack());
+
+ EditTextUtil.addGraphemeClusterLimitFilter(givenName, NAME_MAX_GLYPHS);
+ EditTextUtil.addGraphemeClusterLimitFilter(familyName, NAME_MAX_GLYPHS);
+
+ this.givenName.addTextChangedListener(new AfterTextChanged(EditProfileNameFragment::trimFieldToMaxByteLength));
+ this.familyName.addTextChangedListener(new AfterTextChanged(EditProfileNameFragment::trimFieldToMaxByteLength));
+
+ view.findViewById(R.id.edit_profile_name_save).setOnClickListener(this::onSaveClicked);
+ }
+
+ private void onSaveClicked(View view) {
+ ProfileName profileName = ProfileName.fromParts(givenName.getText().toString(), familyName.getText().toString());
+
+ SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
+ DatabaseFactory.getRecipientDatabase(requireContext()).setProfileName(Recipient.self().getId(), profileName);
+ ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
+ return null;
+ }, (nothing) -> {
+ Navigation.findNavController(view).popBackStack();
+ });
+ }
+
+ public static void trimFieldToMaxByteLength(Editable s) {
+ int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileName.MAX_PART_LENGTH).length();
+
+ if (s.length() > trimmedLength) {
+ s.delete(trimmedLength, s.length());
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java
new file mode 100644
index 0000000000..b2d3e4d0fa
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java
@@ -0,0 +1,64 @@
+package org.thoughtcrime.securesms.profiles.manage;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.navigation.NavDirections;
+import androidx.navigation.NavGraph;
+import androidx.navigation.Navigation;
+
+import org.thoughtcrime.securesms.BaseActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
+import org.thoughtcrime.securesms.profiles.edit.EditProfileFragmentDirections;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+/**
+ * Activity that manages the local user's profile, as accessed via the settings.
+ */
+public class ManageProfileActivity extends BaseActivity {
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+
+ public static final String START_AT_USERNAME = "start_at_username";
+
+ public static @NonNull Intent getIntent(@NonNull Context context) {
+ return new Intent(context, ManageProfileActivity.class);
+ }
+
+ public static @NonNull Intent getIntentForUsernameEdit(@NonNull Context context) {
+ Intent intent = new Intent(context, ManageProfileActivity.class);
+ intent.putExtra(START_AT_USERNAME, true);
+ return intent;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ dynamicTheme.onCreate(this);
+
+ setContentView(R.layout.manage_profile_activity);
+
+ if (bundle == null) {
+ Bundle extras = getIntent().getExtras();
+ NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
+
+ Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle());
+
+ if (extras != null && extras.getBoolean(START_AT_USERNAME, false)) {
+ NavDirections action = ManageProfileFragmentDirections.actionManageUsername();
+ Navigation.findNavController(this, R.id.nav_host_fragment).navigate(action);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java
new file mode 100644
index 0000000000..d061b4c65b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java
@@ -0,0 +1,182 @@
+package org.thoughtcrime.securesms.profiles.manage;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.navigation.Navigation;
+
+import com.bumptech.glide.Glide;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.LoggingFragment;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.profiles.ProfileName;
+import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
+import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
+
+import static android.app.Activity.RESULT_OK;
+
+public class ManageProfileFragment extends LoggingFragment {
+
+ private static final String TAG = Log.tag(ManageProfileFragment.class);
+ private static final short REQUEST_CODE_SELECT_AVATAR = 31726;
+
+ private Toolbar toolbar;
+ private ImageView avatarView;
+ private View avatarPlaceholderView;
+ private TextView profileNameView;
+ private View profileNameContainer;
+ private TextView usernameView;
+ private View usernameContainer;
+ private TextView aboutView;
+ private View aboutContainer;
+ private TextView aboutEmojiView;
+ private AlertDialog avatarProgress;
+
+ private ManageProfileViewModel viewModel;
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.manage_profile_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ this.toolbar = view.findViewById(R.id.toolbar);
+ this.avatarView = view.findViewById(R.id.manage_profile_avatar);
+ this.avatarPlaceholderView = view.findViewById(R.id.manage_profile_avatar_placeholder);
+ this.profileNameView = view.findViewById(R.id.manage_profile_name);
+ this.profileNameContainer = view.findViewById(R.id.manage_profile_name_container);
+ this.usernameView = view.findViewById(R.id.manage_profile_username);
+ this.usernameContainer = view.findViewById(R.id.manage_profile_username_container);
+ this.aboutView = view.findViewById(R.id.manage_profile_about);
+ this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
+
+ initializeViewModel();
+
+ this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
+ this.avatarView.setOnClickListener(v -> onAvatarClicked());
+
+ this.profileNameContainer.setOnClickListener(v -> {
+ Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageProfileName());
+ });
+
+ this.usernameContainer.setOnClickListener(v -> {
+ Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageUsername());
+ });
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) {
+ if (data != null && data.getBooleanExtra("delete", false)) {
+ viewModel.onAvatarSelected(requireContext(), null);
+ return;
+ }
+
+ Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
+
+ viewModel.onAvatarSelected(requireContext(), result);
+ }
+ }
+
+ private void initializeViewModel() {
+ viewModel = ViewModelProviders.of(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class);
+
+ viewModel.getAvatar().observe(getViewLifecycleOwner(), this::presentAvatar);
+ viewModel.getProfileName().observe(getViewLifecycleOwner(), this::presentProfileName);
+ viewModel.getEvents().observe(getViewLifecycleOwner(), this::presentEvent);
+
+ if (viewModel.shouldShowAbout()) {
+ viewModel.getAbout().observe(getViewLifecycleOwner(), this::presentAbout);
+ viewModel.getAboutEmoji().observe(getViewLifecycleOwner(), this::presentAboutEmoji);
+ } else {
+ aboutContainer.setVisibility(View.GONE);
+ }
+
+ if (viewModel.shouldShowUsername()) {
+ viewModel.getUsername().observe(getViewLifecycleOwner(), this::presentUsername);
+ } else {
+ usernameContainer.setVisibility(View.GONE);
+ }
+ }
+
+ private void presentAvatar(@NonNull AvatarState avatarState) {
+ if (avatarState.getAvatar() == null) {
+ avatarView.setImageDrawable(null);
+ avatarPlaceholderView.setVisibility(View.VISIBLE);
+ } else {
+ avatarPlaceholderView.setVisibility(View.GONE);
+ Glide.with(this)
+ .load(avatarState.getAvatar())
+ .circleCrop()
+ .into(avatarView);
+ }
+
+ if (avatarProgress == null && avatarState.getLoadingState() == ManageProfileViewModel.LoadingState.LOADING) {
+ avatarProgress = SimpleProgressDialog.show(requireContext());
+ } else if (avatarProgress != null && avatarState.getLoadingState() == ManageProfileViewModel.LoadingState.LOADED) {
+ avatarProgress.dismiss();
+ }
+ }
+
+ private void presentProfileName(@Nullable ProfileName profileName) {
+ if (profileName == null || profileName.isEmpty()) {
+ profileNameView.setText(R.string.ManageProfileFragment_profile_name);
+ } else {
+ profileNameView.setText(profileName.toString());
+ }
+ }
+
+ private void presentUsername(@Nullable String username) {
+ if (username == null || username.isEmpty()) {
+ usernameView.setText(R.string.ManageProfileFragment_username);
+ usernameView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_secondary));
+ } else {
+ usernameView.setText(username);
+ usernameView.setTextColor(requireContext().getResources().getColor(R.color.signal_text_primary));
+ }
+ }
+
+ private void presentAbout(@Nullable String about) {
+ if (about == null || about.isEmpty()) {
+ aboutView.setHint(R.string.ManageProfileFragment_about);
+ } else {
+ aboutView.setText(about);
+ }
+ }
+
+ private void presentAboutEmoji(@NonNull String aboutEmoji) {
+
+ }
+
+ private void presentEvent(@NonNull ManageProfileViewModel.Event event) {
+ if (event == ManageProfileViewModel.Event.AVATAR_FAILURE) {
+ Toast.makeText(requireContext(), R.string.ManageProfileFragment_failed_to_set_avatar, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void onAvatarClicked() {
+ AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveAvatar(),
+ true,
+ REQUEST_CODE_SELECT_AVATAR,
+ false)
+ .show(getChildFragmentManager(), null);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java
new file mode 100644
index 0000000000..f3cf6e0455
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java
@@ -0,0 +1,191 @@
+package org.thoughtcrime.securesms.profiles.manage;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.signal.core.util.StreamUtil;
+import org.signal.core.util.concurrent.SignalExecutors;
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
+import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.profiles.AvatarHelper;
+import org.thoughtcrime.securesms.profiles.ProfileName;
+import org.thoughtcrime.securesms.providers.BlobProvider;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
+import org.thoughtcrime.securesms.util.FeatureFlags;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.whispersystems.signalservice.api.util.StreamDetails;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+class ManageProfileViewModel extends ViewModel {
+
+ private static final String TAG = Log.tag(ManageProfileViewModel.class);
+
+ private final MutableLiveData avatar;
+ private final MutableLiveData profileName;
+ private final MutableLiveData username;
+ private final MutableLiveData about;
+ private final MutableLiveData aboutEmoji;
+ private final SingleLiveEvent events;
+ private final RecipientForeverObserver observer;
+
+ public ManageProfileViewModel() {
+ this.avatar = new MutableLiveData<>();
+ this.profileName = new MutableLiveData<>();
+ this.username = new MutableLiveData<>();
+ this.about = new MutableLiveData<>();
+ this.aboutEmoji = new MutableLiveData<>();
+ this.events = new SingleLiveEvent<>();
+ this.observer = this::onRecipientChanged;
+
+ SignalExecutors.BOUNDED.execute(() -> {
+ onRecipientChanged(Recipient.self().fresh());
+
+ StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
+ if (details != null) {
+ try {
+ avatar.postValue(AvatarState.loaded(StreamUtil.readFully(details.getStream())));
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read avatar!");
+ avatar.postValue(AvatarState.none());
+ }
+ } else {
+ avatar.postValue(AvatarState.none());
+ }
+
+ ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
+ });
+
+ Recipient.self().live().observeForever(observer);
+ }
+
+ public @NonNull LiveData getAvatar() {
+ return avatar;
+ }
+
+ public @NonNull LiveData getProfileName() {
+ return profileName;
+ }
+
+ public @NonNull LiveData getUsername() {
+ return username;
+ }
+
+ public @NonNull LiveData getAbout() {
+ return about;
+ }
+
+ public @NonNull LiveData getAboutEmoji() {
+ return aboutEmoji;
+ }
+
+ public @NonNull LiveData getEvents() {
+ return events;
+ }
+
+ public boolean shouldShowUsername() {
+ return FeatureFlags.usernames();
+ }
+
+ public boolean shouldShowAbout() {
+ return FeatureFlags.about();
+ }
+
+ public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
+ if (media == null) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ AvatarHelper.delete(context, Recipient.self().getId());
+ avatar.postValue(AvatarState.none());
+ ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
+ });
+ } else {
+ SignalExecutors.BOUNDED.execute(() -> {
+ try {
+ InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
+ byte[] data = StreamUtil.readFully(stream);
+
+ AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data));
+ avatar.postValue(AvatarState.loaded(data));
+
+ ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to save avatar!", e);
+ events.postValue(Event.AVATAR_FAILURE);
+ }
+ });
+ }
+ }
+
+ public boolean canRemoveAvatar() {
+ return avatar.getValue() != null;
+ }
+
+ private void onRecipientChanged(@NonNull Recipient recipient) {
+ profileName.postValue(recipient.getProfileName());
+ username.postValue(recipient.getUsername().orNull());
+ }
+
+ @Override
+ protected void onCleared() {
+ Recipient.self().live().removeForeverObserver(observer);
+ }
+
+ public static class AvatarState {
+ private final byte[] avatar;
+ private final LoadingState loadingState;
+
+ public AvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
+ this.avatar = avatar;
+ this.loadingState = loadingState;
+ }
+
+ private static @NonNull AvatarState none() {
+ return new AvatarState(null, LoadingState.LOADED);
+ }
+
+ private static @NonNull AvatarState loaded(@Nullable byte[] avatar) {
+ return new AvatarState(avatar, LoadingState.LOADED);
+ }
+
+ private static @NonNull AvatarState loading(@Nullable byte[] avatar) {
+ return new AvatarState(avatar, LoadingState.LOADING);
+ }
+
+ public @Nullable byte[] getAvatar() {
+ return avatar;
+ }
+
+ public LoadingState getLoadingState() {
+ return loadingState;
+ }
+ }
+
+ public enum LoadingState {
+ LOADING, LOADED
+ }
+
+ enum Event {
+ AVATAR_FAILURE
+ }
+
+ static class Factory extends ViewModelProvider.NewInstanceFactory {
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return Objects.requireNonNull(modelClass.cast(new ManageProfileViewModel()));
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
similarity index 99%
rename from app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditFragment.java
rename to app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
index fb54ccd749..4d4c176b9c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.usernames.username;
+package org.thoughtcrime.securesms.profiles.manage;
import android.os.Bundle;
import android.view.LayoutInflater;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java
similarity index 98%
rename from app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditRepository.java
rename to app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java
index f117b0ebab..430256b6c2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditRepository.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.usernames.username;
+package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java
similarity index 99%
rename from app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditViewModel.java
rename to app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java
index b0046a13aa..c6873cec3f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/username/UsernameEditViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.usernames.username;
+package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
import android.text.TextUtils;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index c9164d51fa..f75dc05f02 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -71,6 +71,7 @@ public final class FeatureFlags {
private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval";
private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
+ private static final String ABOUT = "android.about";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -98,7 +99,8 @@ public final class FeatureFlags {
AUTOMATIC_SESSION_RESET,
AUTOMATIC_SESSION_INTERVAL,
DEFAULT_MAX_BACKOFF,
- OKHTTP_AUTOMATIC_RETRY
+ OKHTTP_AUTOMATIC_RETRY,
+ ABOUT
);
@VisibleForTesting
@@ -136,7 +138,8 @@ public final class FeatureFlags {
AUTOMATIC_SESSION_RESET,
AUTOMATIC_SESSION_INTERVAL,
DEFAULT_MAX_BACKOFF,
- OKHTTP_AUTOMATIC_RETRY
+ OKHTTP_AUTOMATIC_RETRY,
+ ABOUT
);
/**
@@ -316,6 +319,11 @@ public final class FeatureFlags {
return getBoolean(OKHTTP_AUTOMATIC_RETRY, false);
}
+ /** Whether or not the 'About' section of the profile is enabled. */
+ public static boolean about() {
+ return getBoolean(ABOUT, false);
+ }
+
/** Only for rendering debug info. */
public static synchronized @NonNull Map getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);
diff --git a/app/src/main/res/drawable-night/ic_compose_24.xml b/app/src/main/res/drawable-night/ic_compose_24.xml
new file mode 100644
index 0000000000..6c06b6e39d
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_compose_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_profile_name_24.xml b/app/src/main/res/drawable-night/ic_profile_name_24.xml
new file mode 100644
index 0000000000..809476751e
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_profile_name_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_compose_24.xml b/app/src/main/res/drawable/ic_compose_24.xml
new file mode 100644
index 0000000000..3112150a36
--- /dev/null
+++ b/app/src/main/res/drawable/ic_compose_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_name_24.xml b/app/src/main/res/drawable/ic_profile_name_24.xml
new file mode 100644
index 0000000000..23dc66e2c7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_name_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/edit_profile_name_fragment.xml b/app/src/main/res/layout/edit_profile_name_fragment.xml
new file mode 100644
index 0000000000..00935281df
--- /dev/null
+++ b/app/src/main/res/layout/edit_profile_name_fragment.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/manage_profile_activity.xml b/app/src/main/res/layout/manage_profile_activity.xml
new file mode 100644
index 0000000000..f8ac740ca3
--- /dev/null
+++ b/app/src/main/res/layout/manage_profile_activity.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/manage_profile_fragment.xml b/app/src/main/res/layout/manage_profile_fragment.xml
new file mode 100644
index 0000000000..e1d0ec1ac7
--- /dev/null
+++ b/app/src/main/res/layout/manage_profile_fragment.xml
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/profile_create_fragment.xml b/app/src/main/res/layout/profile_create_fragment.xml
index 4f6df2e3de..d6f8797184 100644
--- a/app/src/main/res/layout/profile_create_fragment.xml
+++ b/app/src/main/res/layout/profile_create_fragment.xml
@@ -165,54 +165,6 @@
android:singleLine="true" />
-
-
-
-
-
-
diff --git a/app/src/main/res/navigation/edit_profile.xml b/app/src/main/res/navigation/edit_profile.xml
index 3f21938693..a37ec5fb93 100644
--- a/app/src/main/res/navigation/edit_profile.xml
+++ b/app/src/main/res/navigation/edit_profile.xml
@@ -23,7 +23,7 @@
diff --git a/app/src/main/res/navigation/manage_profile.xml b/app/src/main/res/navigation/manage_profile.xml
new file mode 100644
index 0000000000..bff808c09a
--- /dev/null
+++ b/app/src/main/res/navigation/manage_profile.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index abcc8612ed..a727d72e23 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -802,7 +802,16 @@
Default (Don\'t notify me)
Always notify me
Don\'t notify me
-
+
+
+ Profile name
+ Username
+ About
+ Write a few words about yourself
+ Your name
+ Your username
+ Failed to set avatar
+
Add to system contacts
This person is in your contacts
@@ -2177,6 +2186,12 @@
Group name
https://support.signal.org/hc/articles/360007459591
+
+ Your name
+ First name
+ Last name (optional)
+ Save
+
Shared media
diff --git a/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java b/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java
index 577b867c89..30d08da6e0 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java
+++ b/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java
@@ -167,10 +167,10 @@ public final class ProfileNameTest {
@Test
public void fromParts_with_long_name_parts() {
- ProfileName name = ProfileName.fromParts("GivenSomeVeryLongNameSomeVeryLongName", "FamilySomeVeryLongNameSomeVeryLongName");
+ ProfileName name = ProfileName.fromParts("GivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLongNameSomeVeryLongName", "FamilySomeVeryLongNameSomeVeryLongName");
- assertEquals("GivenSomeVeryLongNameSomeV", name.getGivenName());
- assertEquals("FamilySomeVeryLongNameSome", name.getFamilyName());
+ assertEquals("GivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLongNameSomeVeryLongNameGivenSomeVeryLong", name.getGivenName());
+ assertEquals("FamilySomeVeryLongNameSomeVeryLongName", name.getFamilyName());
}
@Test
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
index 5c949e0d40..6ce1229ccb 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java
@@ -639,7 +639,7 @@ public class SignalServiceAccountManager {
{
if (name == null) name = "";
- byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH);
+ byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.getTargetNameLength(name));
boolean hasAvatar = avatar != null;
ProfileAvatarData profileAvatarData = null;
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
index 2e16aecbf9..446c6dd832 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
@@ -5,6 +5,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.internal.util.Util;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -20,7 +21,10 @@ import javax.crypto.spec.SecretKeySpec;
public class ProfileCipher {
- public static final int NAME_PADDED_LENGTH = 53;
+ private static final int NAME_PADDED_LENGTH_1 = 53;
+ private static final int NAME_PADDED_LENGTH_2 = 257;
+
+ public static final int MAX_POSSIBLE_NAME_LENGTH = NAME_PADDED_LENGTH_2;
private final ProfileKey key;
@@ -99,4 +103,13 @@ public class ProfileCipher {
}
}
+ public static int getTargetNameLength(String name) {
+ int nameLength = name.getBytes(StandardCharsets.UTF_8).length;
+
+ if (nameLength <= NAME_PADDED_LENGTH_1) {
+ return NAME_PADDED_LENGTH_1;
+ } else {
+ return NAME_PADDED_LENGTH_2;
+ }
+ }
}
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
index 62c7b0f3ef..c03ac68b1c 100644
--- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
@@ -7,9 +7,11 @@ import org.conscrypt.Conscrypt;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
+import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
import java.security.Security;
public class ProfileCipherTest extends TestCase {
@@ -21,7 +23,7 @@ public class ProfileCipherTest extends TestCase {
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
- byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
+ byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), 53);
byte[] plaintext = cipher.decryptName(name);
assertEquals(new String(plaintext), "Clement\0Duval");
}
@@ -59,4 +61,29 @@ public class ProfileCipherTest extends TestCase {
assertEquals(new String(result.toByteArray()), "This is an avatar");
}
+ public void testEncryptLengthBucket1() throws InvalidInputException {
+ ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
+ ProfileCipher cipher = new ProfileCipher(key);
+ byte[] name = cipher.encryptName("Peter\0Parker".getBytes(), 53);
+
+ String encoded = Base64.encodeBytes(name);
+
+ assertEquals(108, encoded.length());
+ }
+
+ public void testEncryptLengthBucket2() throws InvalidInputException {
+ ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
+ ProfileCipher cipher = new ProfileCipher(key);
+ byte[] name = cipher.encryptName("Peter\0Parker".getBytes(), 257);
+
+ String encoded = Base64.encodeBytes(name);
+
+ assertEquals(380, encoded.length());
+ }
+
+ public void testTargetNameLength() {
+ assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1"));
+ assertEquals(257, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12"));
+ }
}