Create a new manage profile screen.

This commit is contained in:
Greyson Parrelli 2021-01-14 13:05:03 -05:00
parent 7e64d57ba8
commit 8ca54bcc7b
30 changed files with 1004 additions and 158 deletions

View file

@ -460,6 +460,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.manage.ManageProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"

View file

@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CachedInflater;
@ -345,7 +346,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntent(requireActivity()));
return true;
}
}
@ -353,7 +354,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntentForUsernameEdit(preference.getContext()));
return true;
}
}

View file

@ -18,7 +18,7 @@ import java.util.Objects;
public final class ProfileName implements Parcelable {
public static final ProfileName EMPTY = new ProfileName("", "");
public static final int MAX_PART_LENGTH = (ProfileCipher.NAME_PADDED_LENGTH - 1) / 2;
public static final int MAX_PART_LENGTH = (ProfileCipher.MAX_POSSIBLE_NAME_LENGTH - 1) / 2;
private final String givenName;
private final String familyName;

View file

@ -17,16 +17,17 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Shows editing screen for your profile during registration. Also handles group name editing.
*/
@SuppressLint("StaticFieldLeak")
public class EditProfileActivity extends BaseActivity implements EditProfileFragment.Controller {
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
public static final String SHOW_TOOLBAR = "show_back_arrow";
public static final String GROUP_ID = "group_id";
public static final String START_AT_USERNAME = "start_at_username";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
@ -39,7 +40,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
public static @NonNull Intent getIntentForUserProfileEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
return intent;
}
@ -52,14 +52,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
return intent;
}
public static @NonNull Intent getIntentForUsernameEdit(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.START_AT_USERNAME, true);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
@ -73,13 +65,6 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag
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(DISPLAY_USERNAME, false) &&
extras.getBoolean(START_AT_USERNAME, false)) {
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(action);
}
}
}

View file

@ -18,11 +18,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
@ -39,6 +36,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
@ -53,7 +51,6 @@ import java.io.IOException;
import java.io.InputStream;
import static android.app.Activity.RESULT_OK;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPLAY_USERNAME;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.GROUP_ID;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
@ -73,9 +70,6 @@ public class EditProfileFragment extends LoggingFragment {
private EditText familyName;
private View reveal;
private TextView preview;
private View usernameLabel;
private View usernameEditButton;
private TextView username;
private Intent nextIntent;
@ -94,26 +88,8 @@ public class EditProfileFragment extends LoggingFragment {
}
}
public static EditProfileFragment create(boolean excludeSystem,
Intent nextIntent,
boolean displayUsernameField,
@StringRes int nextButtonText) {
EditProfileFragment fragment = new EditProfileFragment();
Bundle args = new Bundle();
args.putBoolean(EXCLUDE_SYSTEM, excludeSystem);
args.putParcelable(NEXT_INTENT, nextIntent);
args.putBoolean(DISPLAY_USERNAME, displayUsernameField);
args.putInt(NEXT_BUTTON_TEXT, nextButtonText);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.profile_create_fragment, container, false);
}
@ -125,13 +101,6 @@ public class EditProfileFragment extends LoggingFragment {
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), groupId, savedInstanceState != null);
initializeProfileAvatar();
initializeProfileName();
initializeUsername();
}
@Override
public void onResume() {
super.onResume();
viewModel.refreshUsername();
}
@Override
@ -200,17 +169,8 @@ public class EditProfileFragment extends LoggingFragment {
this.finishButton = view.findViewById(R.id.finish_button);
this.reveal = view.findViewById(R.id.reveal);
this.preview = view.findViewById(R.id.name_preview);
this.username = view.findViewById(R.id.profile_overview_username);
this.usernameEditButton = view.findViewById(R.id.profile_overview_username_edit_button);
this.usernameLabel = view.findViewById(R.id.profile_overview_username_label);
this.nextIntent = arguments.getParcelable(NEXT_INTENT);
if (FeatureFlags.usernames() && arguments.getBoolean(DISPLAY_USERNAME, false)) {
username.setVisibility(View.VISIBLE);
usernameEditButton.setVisibility(View.VISIBLE);
usernameLabel.setVisibility(View.VISIBLE);
}
this.avatar.setOnClickListener(v -> 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.<ImageView>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<String> 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();
}

View file

@ -27,7 +27,6 @@ class EditProfileViewModel extends ViewModel {
private final LiveData<ProfileName> internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
private final LiveData<Boolean> isFormValid;
private final EditProfileRepository repository;
@ -77,10 +76,6 @@ class EditProfileViewModel extends ViewModel {
return Transformations.distinctUntilChanged(internalAvatar);
}
public LiveData<Optional<String>> 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<EditProfileRepository.UploadResult> uploadResultConsumer) {
ProfileName profileName = isGroup() ? ProfileName.EMPTY : internalProfileName.getValue();
String displayName = isGroup() ? givenName.getValue() : "";

View file

@ -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);

View file

@ -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.<Toolbar>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());
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<AvatarState> avatar;
private final MutableLiveData<ProfileName> profileName;
private final MutableLiveData<String> username;
private final MutableLiveData<String> about;
private final MutableLiveData<String> aboutEmoji;
private final SingleLiveEvent<Event> 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<AvatarState> getAvatar() {
return avatar;
}
public @NonNull LiveData<ProfileName> getProfileName() {
return profileName;
}
public @NonNull LiveData<String> getUsername() {
return username;
}
public @NonNull LiveData<String> getAbout() {
return about;
}
public @NonNull LiveData<String> getAboutEmoji() {
return aboutEmoji;
}
public @NonNull LiveData<Event> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new ManageProfileViewModel()));
}
}
}

View file

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.os.Bundle;
import android.view.LayoutInflater;

View file

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;

View file

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.usernames.username;
package org.thoughtcrime.securesms.profiles.manage;
import android.app.Application;
import android.text.TextUtils;

View file

@ -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<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M21.561,6.682 L19.086,9.157 14.843,4.914l2.475,-2.475a1.5,1.5 0,0 1,2.121 0l2.122,2.122A1.5,1.5 0,0 1,21.561 6.682ZM3.429,16.631 L2.317,21.076a0.5,0.5 0,0 0,0.607 0.607l4.445,-1.112a1.5,1.5 0,0 0,0.7 -0.394l9.959,-9.959L13.782,5.975 3.823,15.934A1.5,1.5 0,0 0,3.429 16.631Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2.5C11.0258,2.5 10.0734,2.7943 9.2633,3.3458C8.4533,3.8973 7.8219,4.6811 7.4491,5.5982C7.0763,6.5153 6.9787,7.5244 7.1688,8.498C7.3588,9.4716 7.828,10.3658 8.5169,11.0677C9.2058,11.7696 10.0835,12.2476 11.039,12.4413C11.9946,12.635 12.985,12.5356 13.8851,12.1557C14.7852,11.7758 15.5545,11.1326 16.0958,10.3072C16.6371,9.4818 16.926,8.5115 16.926,7.5189C16.926,6.1878 16.407,4.9112 15.4832,3.97C14.5594,3.0288 13.3065,2.5 12,2.5ZM16.9808,12.5765C16.3283,13.2458 15.5523,13.777 14.6975,14.1395C13.8427,14.502 12.9259,14.6886 12,14.6886C11.0741,14.6886 10.1573,14.502 9.3025,14.1395C8.4477,13.777 7.6717,13.2458 7.0192,12.5765C5.7036,13.0321 4.5608,13.8953 3.7504,15.0454C2.9402,16.1955 2.5029,17.5749 2.5,18.9906V20.066C2.5,20.4463 2.6483,20.8111 2.9122,21.08C3.1762,21.3489 3.5341,21.5 3.9074,21.5H20.0926C20.4659,21.5 20.8238,21.3489 21.0878,21.08C21.3517,20.8111 21.5,20.4463 21.5,20.066V18.9906C21.4971,17.5749 21.0599,16.1955 20.2495,15.0454C19.4392,13.8953 18.2964,13.0321 16.9808,12.5765Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M21.561,4.561 L19.439,2.439a1.5,1.5 0,0 0,-2.121 0L3.823,15.934a1.5,1.5 0,0 0,-0.394 0.7L2.317,21.076a0.5,0.5 0,0 0,0.607 0.607l4.445,-1.112a1.5,1.5 0,0 0,0.7 -0.394l13.5,-13.495A1.5,1.5 0,0 0,21.561 4.561ZM7.005,19.116l-2.828,0.707L4.884,17l9.772,-9.773 2.122,2.122ZM17.838,8.283 L15.717,6.162 18.379,3.5 20.5,5.621Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16.3836,11.8716C17.1821,11.0172 17.7135,9.9481 17.9125,8.7958C18.1115,7.6434 17.9694,6.458 17.5037,5.3853C17.038,4.3126 16.269,3.3994 15.2912,2.758C14.3134,2.1165 13.1694,1.7748 12,1.7748C10.8306,1.7748 9.6867,2.1165 8.7089,2.758C7.7311,3.3994 6.9621,4.3126 6.4963,5.3853C6.0306,6.458 5.8885,7.6434 6.0875,8.7958C6.2865,9.9481 6.8179,11.0172 7.6164,11.8716C6.1365,12.2089 4.8148,13.0382 3.8673,14.2239C2.9198,15.4097 2.4025,16.8818 2.4,18.3996V20.4H4.2V18.3996C4.2016,17.1006 4.7183,15.8553 5.6368,14.9368C6.5553,14.0183 7.8007,13.5016 9.0996,13.5H14.9004C16.1994,13.5016 17.4447,14.0183 18.3632,14.9368C19.2817,15.8553 19.7984,17.1006 19.8,18.3996V20.4H21.6V18.3996C21.5975,16.8818 21.0802,15.4097 20.1327,14.2239C19.1852,13.0382 17.8635,12.2089 16.3836,11.8716V11.8716ZM12,12C11.1693,12 10.3573,11.7537 9.6666,11.2922C8.9759,10.8307 8.4376,10.1747 8.1197,9.4073C7.8018,8.6398 7.7187,7.7953 7.8807,6.9806C8.0428,6.1659 8.4428,5.4175 9.0302,4.8302C9.6176,4.2428 10.3659,3.8428 11.1806,3.6807C11.9954,3.5186 12.8398,3.6018 13.6073,3.9197C14.3747,4.2376 15.0307,4.7759 15.4922,5.4666C15.9537,6.1573 16.2,6.9693 16.2,7.8C16.2,8.9139 15.7575,9.9822 14.9699,10.7698C14.1822,11.5575 13.1139,12 12,12Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/EditProfileNameFragment_your_name" />
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/edit_profile_name_given_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
style="@style/Signal.Text.Body"
android:hint="@string/EditProfileNameFragment_first_name"
android:inputType="textPersonName"
android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"/>
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/edit_profile_name_family_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
style="@style/Signal.Text.Body"
android:hint="@string/EditProfileNameFragment_last_name_optional"
android:inputType="textPersonName"
android:maxLines="1"
app:layout_constraintTop_toBottomOf="@id/edit_profile_name_given_name"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/edit_profile_name_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
style="@style/Signal.Widget.Button.Medium.Primary"
android:text="@string/EditProfileNameFragment_save"
app:cornerRadius="80dp"
app:elevation="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".profiles.edit.EditProfileActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/manage_profile" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/CreateProfileActivity__profile" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/manage_profile_avatar_background"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginTop="33dp"
android:src="@drawable/circle_tintable"
android:tint="@color/core_grey_05"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_goneMarginTop="?attr/actionBarSize" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/manage_profile_avatar_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:tint="@color/core_grey_75"
app:layout_constraintBottom_toBottomOf="@+id/manage_profile_avatar_background"
app:layout_constraintEnd_toEndOf="@+id/manage_profile_avatar_background"
app:layout_constraintStart_toStartOf="@+id/manage_profile_avatar_background"
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background"
app:srcCompat="@drawable/ic_profile_outline_40" />
<ImageView
android:id="@+id/manage_profile_avatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/CreateProfileActivity_set_avatar_description"
app:layout_constraintBottom_toBottomOf="@+id/manage_profile_avatar_background"
app:layout_constraintEnd_toEndOf="@+id/manage_profile_avatar_background"
app:layout_constraintStart_toStartOf="@+id/manage_profile_avatar_background"
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background" />
<ImageView
android:id="@+id/manage_profile_camera_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="56dp"
android:layout_marginTop="56dp"
android:background="@drawable/circle_tintable_padded"
android:cropToPadding="false"
android:elevation="4dp"
android:padding="14dp"
app:backgroundTint="@color/camera_icon_background_tint"
app:layout_constraintStart_toStartOf="@+id/manage_profile_avatar_background"
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background"
app:srcCompat="@drawable/ic_camera_24" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/manage_profile_name_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="33dp"
android:paddingStart="26dp"
android:paddingEnd="26dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="?selectableItemBackground"
app:layout_constraintTop_toBottomOf="@id/manage_profile_avatar">
<ImageView
android:id="@+id/manage_profile_name_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_profile_name_24"
app:tint="@color/signal_text_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/manage_profile_name"
app:layout_constraintBottom_toBottomOf="@id/manage_profile_name_subtitle"/>
<TextView
android:id="@+id/manage_profile_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
style="@style/Signal.Text.Body"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/manage_profile_name_icon"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Peter Parker"/>
<TextView
android:id="@+id/manage_profile_name_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Signal.Text.Preview"
android:text="@string/ManageProfileFragment_your_name"
android:textColor="@color/signal_text_secondary"
app:layout_constraintTop_toBottomOf="@id/manage_profile_name"
app:layout_constraintStart_toStartOf="@id/manage_profile_name"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/manage_profile_username_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="26dp"
android:paddingEnd="26dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="?selectableItemBackground"
app:layout_constraintTop_toBottomOf="@id/manage_profile_name_container">
<ImageView
android:id="@+id/manage_profile_username_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_at_24"
app:tint="@color/signal_text_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/manage_profile_username"
app:layout_constraintBottom_toBottomOf="@id/manage_profile_username_subtitle"/>
<TextView
android:id="@+id/manage_profile_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
style="@style/Signal.Text.Body"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/manage_profile_username_icon"
app:layout_constraintEnd_toEndOf="parent"
tools:text="\@spiderman"/>
<TextView
android:id="@+id/manage_profile_username_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Signal.Text.Preview"
android:text="@string/ManageProfileFragment_your_username"
android:textColor="@color/signal_text_secondary"
app:layout_constraintTop_toBottomOf="@id/manage_profile_username"
app:layout_constraintStart_toStartOf="@id/manage_profile_username"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/manage_profile_about_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="26dp"
android:paddingEnd="26dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="?selectableItemBackground"
app:layout_constraintTop_toBottomOf="@id/manage_profile_username_container">
<ImageView
android:id="@+id/manage_profile_about_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_compose_24"
app:tint="@color/signal_text_primary"
app:layout_constraintTop_toTopOf="@id/manage_profile_about"
app:layout_constraintBottom_toBottomOf="@id/manage_profile_about_subtitle"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/manage_profile_about"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="26dp"
style="@style/Signal.Text.Body"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/manage_profile_about_icon"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Reporter for the Daily Bugle"/>
<TextView
android:id="@+id/manage_profile_about_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Signal.Text.Preview"
android:text="@string/ManageProfileFragment_write_a_few_words_about_yourself"
android:textColor="@color/signal_text_secondary"
app:layout_constraintTop_toBottomOf="@id/manage_profile_about"
app:layout_constraintStart_toStartOf="@id/manage_profile_about"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/description_text"
style="@style/Signal.Text.Preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="33dp"
android:layout_marginBottom="16dp"
android:padding="26dp"
android:text="@string/CreateProfileActivity_signal_profiles_are_end_to_end_encrypted"
android:textColor="@color/signal_text_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manage_profile_about_container"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -165,54 +165,6 @@
android:singleLine="true" />
</LinearLayout>
<TextView
android:id="@+id/profile_overview_username_label"
style="@style/Signal.Text.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@string/CreateProfileActivity__username"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name_container"
tools:visibility="visible" />
<EditText
android:id="@+id/profile_overview_username"
style="@style/Signal.Text.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:background="@null"
android:editable="false"
android:focusable="false"
android:hint="@string/CreateProfileActivity__create_a_username"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/profile_overview_username_edit_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/profile_overview_username_edit_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="8dp"
android:tint="@color/core_grey_55"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/profile_overview_username"
app:srcCompat="@drawable/ic_compose_solid_24"
tools:visibility="visible" />
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/description_text"
style="@style/Signal.Text.Preview"
@ -226,7 +178,7 @@
android:textColor="@color/core_grey_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profile_overview_username_edit_button"
app:layout_constraintTop_toBottomOf="@+id/name_container"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -23,7 +23,7 @@
<fragment
android:id="@+id/usernameEditFragment"
android:name="org.thoughtcrime.securesms.usernames.username.UsernameEditFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment"
android:label="fragment_edit_username"
tools:layout="@layout/username_edit_fragment" />

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/manage_profile"
app:startDestination="@id/manageProfileFragment">
<fragment
android:id="@+id/manageProfileFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.ManageProfileFragment"
android:label="fragment_manage_profile"
tools:layout="@layout/profile_create_fragment">
<action
android:id="@+id/action_manageUsername"
app:destination="@id/usernameManageFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_manageProfileName"
app:destination="@id/profileNameManageFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/usernameManageFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment"
android:label="fragment_manage_username"
tools:layout="@layout/username_edit_fragment" />
<fragment
android:id="@+id/profileNameManageFragment"
android:name="org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment"
android:label="fragment_manage_profile_name"
tools:layout="@layout/edit_profile_name_fragment" />
</navigation>

View file

@ -802,7 +802,16 @@
<string name="GroupMentionSettingDialog_default_dont_notify_me">Default (Don\'t notify me)</string>
<string name="GroupMentionSettingDialog_always_notify_me">Always notify me</string>
<string name="GroupMentionSettingDialog_dont_notify_me">Don\'t notify me</string>
<!-- ManageProfileFragment -->
<string name="ManageProfileFragment_profile_name">Profile name</string>
<string name="ManageProfileFragment_username">Username</string>
<string name="ManageProfileFragment_about">About</string>
<string name="ManageProfileFragment_write_a_few_words_about_yourself">Write a few words about yourself</string>
<string name="ManageProfileFragment_your_name">Your name</string>
<string name="ManageProfileFragment_your_username">Your username</string>
<string name="ManageProfileFragment_failed_to_set_avatar">Failed to set avatar</string>
<!-- ManageRecipientActivity -->
<string name="ManageRecipientActivity_add_to_system_contacts">Add to system contacts</string>
<string name="ManageRecipientActivity_this_person_is_in_your_contacts">This person is in your contacts</string>
@ -2177,6 +2186,12 @@
<string name="EditProfileFragment__group_name">Group name</string>
<string name="EditProfileFragment__support_link" translatable="false">https://support.signal.org/hc/articles/360007459591</string>
<!-- EditProfileNameFragment -->
<string name="EditProfileNameFragment_your_name">Your name</string>
<string name="EditProfileNameFragment_first_name">First name</string>
<string name="EditProfileNameFragment_last_name_optional">Last name (optional)</string>
<string name="EditProfileNameFragment_save">Save</string>
<!-- recipient_preferences_activity -->
<string name="recipient_preference_activity__shared_media">Shared media</string>

View file

@ -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

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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"));
}
}