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

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