mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Create a new manage profile screen.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() : "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
|
||||
public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
private static final float DISABLED_ALPHA = 0.5f;
|
||||
|
||||
private UsernameEditViewModel viewModel;
|
||||
|
||||
private EditText usernameInput;
|
||||
private TextView usernameSubtext;
|
||||
private CircularProgressButton submitButton;
|
||||
private CircularProgressButton deleteButton;
|
||||
|
||||
public static UsernameEditFragment newInstance() {
|
||||
return new UsernameEditFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.username_edit_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
usernameInput = view.findViewById(R.id.username_text);
|
||||
usernameSubtext = view.findViewById(R.id.username_subtext);
|
||||
submitButton = view.findViewById(R.id.username_submit_button);
|
||||
deleteButton = view.findViewById(R.id.username_delete_button);
|
||||
|
||||
view.<Toolbar>findViewById(R.id.toolbar)
|
||||
.setNavigationOnClickListener(v -> Navigation.findNavController(view)
|
||||
.popBackStack());
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new UsernameEditViewModel.Factory()).get(UsernameEditViewModel.class);
|
||||
|
||||
viewModel.getUiState().observe(getViewLifecycleOwner(), this::onUiStateChanged);
|
||||
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
|
||||
|
||||
submitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted(usernameInput.getText().toString()));
|
||||
deleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||
|
||||
usernameInput.setText(Recipient.self().getUsername().orNull());
|
||||
usernameInput.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.onUsernameUpdated(text);
|
||||
}
|
||||
});
|
||||
usernameInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
viewModel.onUsernameSubmitted(usernameInput.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||
usernameInput.setEnabled(true);
|
||||
|
||||
switch (state.getButtonState()) {
|
||||
case SUBMIT:
|
||||
cancelSpinning(submitButton);
|
||||
submitButton.setVisibility(View.VISIBLE);
|
||||
submitButton.setEnabled(true);
|
||||
submitButton.setAlpha(1);
|
||||
deleteButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case SUBMIT_DISABLED:
|
||||
cancelSpinning(submitButton);
|
||||
submitButton.setVisibility(View.VISIBLE);
|
||||
submitButton.setEnabled(false);
|
||||
submitButton.setAlpha(DISABLED_ALPHA);
|
||||
deleteButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case SUBMIT_LOADING:
|
||||
setSpinning(submitButton);
|
||||
submitButton.setVisibility(View.VISIBLE);
|
||||
submitButton.setAlpha(1);
|
||||
deleteButton.setVisibility(View.GONE);
|
||||
usernameInput.setEnabled(false);
|
||||
break;
|
||||
case DELETE:
|
||||
cancelSpinning(deleteButton);
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setEnabled(true);
|
||||
deleteButton.setAlpha(1);
|
||||
submitButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case DELETE_DISABLED:
|
||||
cancelSpinning(deleteButton);
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setEnabled(false);
|
||||
deleteButton.setAlpha(DISABLED_ALPHA);
|
||||
submitButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case DELETE_LOADING:
|
||||
setSpinning(deleteButton);
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setAlpha(1);
|
||||
submitButton.setVisibility(View.GONE);
|
||||
usernameInput.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state.getUsernameStatus()) {
|
||||
case NONE:
|
||||
usernameSubtext.setText("");
|
||||
break;
|
||||
case TOO_SHORT:
|
||||
case TOO_LONG:
|
||||
usernameSubtext.setText(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH));
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
break;
|
||||
case INVALID_CHARACTERS:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_usernames_can_only_include);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
break;
|
||||
case CANNOT_START_WITH_NUMBER:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_usernames_cannot_begin_with_a_number);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
break;
|
||||
case INVALID_GENERIC:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_username_is_invalid);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
break;
|
||||
case TAKEN:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_this_username_is_taken);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
break;
|
||||
case AVAILABLE:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_this_username_is_available);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_green));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onEvent(@NonNull UsernameEditViewModel.Event event) {
|
||||
switch (event) {
|
||||
case SUBMIT_SUCCESS:
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_successfully_set_username, Toast.LENGTH_SHORT).show();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
break;
|
||||
case SUBMIT_FAIL_TAKEN:
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_this_username_is_taken, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case SUBMIT_FAIL_INVALID:
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_username_is_invalid, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case DELETE_SUCCESS:
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_successfully_removed_username, Toast.LENGTH_SHORT).show();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_encountered_a_network_error, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSpinning(@NonNull CircularProgressButton button) {
|
||||
button.setClickable(false);
|
||||
button.setIndeterminateProgressMode(true);
|
||||
button.setProgress(50);
|
||||
}
|
||||
|
||||
private static void cancelSpinning(@NonNull CircularProgressButton button) {
|
||||
button.setProgress(0);
|
||||
button.setIndeterminateProgressMode(false);
|
||||
button.setClickable(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class UsernameEditRepository {
|
||||
|
||||
private static final String TAG = Log.tag(UsernameEditRepository.class);
|
||||
|
||||
private final Application application;
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final Executor executor;
|
||||
|
||||
UsernameEditRepository() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
this.executor = SignalExecutors.UNBOUNDED;
|
||||
}
|
||||
|
||||
void setUsername(@NonNull String username, @NonNull Callback<UsernameSetResult> callback) {
|
||||
executor.execute(() -> callback.onComplete(setUsernameInternal(username)));
|
||||
}
|
||||
|
||||
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
|
||||
executor.execute(() -> callback.onComplete(deleteUsernameInternal()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull UsernameSetResult setUsernameInternal(@NonNull String username) {
|
||||
try {
|
||||
accountManager.setUsername(username);
|
||||
DatabaseFactory.getRecipientDatabase(application).setUsername(Recipient.self().getId(), username);
|
||||
Log.i(TAG, "[setUsername] Successfully set username.");
|
||||
return UsernameSetResult.SUCCESS;
|
||||
} catch (UsernameTakenException e) {
|
||||
Log.w(TAG, "[setUsername] Username taken.");
|
||||
return UsernameSetResult.USERNAME_UNAVAILABLE;
|
||||
} catch (UsernameMalformedException e) {
|
||||
Log.w(TAG, "[setUsername] Username malformed.");
|
||||
return UsernameSetResult.USERNAME_INVALID;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[setUsername] Generic network exception.", e);
|
||||
return UsernameSetResult.NETWORK_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull UsernameDeleteResult deleteUsernameInternal() {
|
||||
try {
|
||||
accountManager.deleteUsername();
|
||||
DatabaseFactory.getRecipientDatabase(application).setUsername(Recipient.self().getId(), null);
|
||||
Log.i(TAG, "[deleteUsername] Successfully deleted the username.");
|
||||
return UsernameDeleteResult.SUCCESS;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[deleteUsername] Generic network exception.", e);
|
||||
return UsernameDeleteResult.NETWORK_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
enum UsernameSetResult {
|
||||
SUCCESS, USERNAME_UNAVAILABLE, USERNAME_INVALID, NETWORK_ERROR
|
||||
}
|
||||
|
||||
enum UsernameDeleteResult {
|
||||
SUCCESS, NETWORK_ERROR
|
||||
}
|
||||
|
||||
enum UsernameAvailableResult {
|
||||
TRUE, FALSE, NETWORK_ERROR
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onComplete(E result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
class UsernameEditViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(UsernameEditViewModel.class);
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<State> uiState;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final UsernameEditRepository repo;
|
||||
|
||||
private UsernameEditViewModel() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.repo = new UsernameEditRepository();
|
||||
this.uiState = new MutableLiveData<>();
|
||||
this.events = new SingleLiveEvent<>();
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
}
|
||||
|
||||
void onUsernameUpdated(@NonNull String username) {
|
||||
if (TextUtils.isEmpty(username) && Recipient.self().getUsername().isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.DELETE, UsernameStatus.NONE));
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.equals(Recipient.self().getUsername().orNull())) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
||||
|
||||
if (invalidReason.isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get())));
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT, UsernameStatus.NONE));
|
||||
}
|
||||
|
||||
void onUsernameSubmitted(@NonNull String username) {
|
||||
if (username.equals(Recipient.self().getUsername().orNull())) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
||||
|
||||
if (invalidReason.isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get())));
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE));
|
||||
|
||||
repo.setUsername(username, (result) -> {
|
||||
Util.runOnMain(() -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
events.postValue(Event.SUBMIT_SUCCESS);
|
||||
break;
|
||||
case USERNAME_INVALID:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC));
|
||||
events.postValue(Event.SUBMIT_FAIL_INVALID);
|
||||
break;
|
||||
case USERNAME_UNAVAILABLE:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN));
|
||||
events.postValue(Event.SUBMIT_FAIL_TAKEN);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT, UsernameStatus.NONE));
|
||||
events.postValue(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void onUsernameDeleted() {
|
||||
uiState.setValue(new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE));
|
||||
|
||||
repo.deleteUsername((result) -> {
|
||||
Util.runOnMain(() -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
uiState.postValue(new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE));
|
||||
events.postValue(Event.DELETE_SUCCESS);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.postValue(new State(ButtonState.DELETE, UsernameStatus.NONE));
|
||||
events.postValue(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<State> getUiState() {
|
||||
return uiState;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {
|
||||
switch (invalidReason) {
|
||||
case TOO_SHORT: return UsernameStatus.TOO_SHORT;
|
||||
case TOO_LONG: return UsernameStatus.TOO_LONG;
|
||||
case STARTS_WITH_NUMBER: return UsernameStatus.CANNOT_START_WITH_NUMBER;
|
||||
case INVALID_CHARACTERS: return UsernameStatus.INVALID_CHARACTERS;
|
||||
default: return UsernameStatus.INVALID_GENERIC;
|
||||
}
|
||||
}
|
||||
|
||||
static class State {
|
||||
private final ButtonState buttonState;
|
||||
private final UsernameStatus usernameStatus;
|
||||
|
||||
private State(@NonNull ButtonState buttonState,
|
||||
@NonNull UsernameStatus usernameStatus)
|
||||
{
|
||||
this.buttonState = buttonState;
|
||||
this.usernameStatus = usernameStatus;
|
||||
}
|
||||
|
||||
@NonNull ButtonState getButtonState() {
|
||||
return buttonState;
|
||||
}
|
||||
|
||||
@NonNull UsernameStatus getUsernameStatus() {
|
||||
return usernameStatus;
|
||||
}
|
||||
}
|
||||
|
||||
enum UsernameStatus {
|
||||
NONE, AVAILABLE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
||||
}
|
||||
|
||||
enum ButtonState {
|
||||
SUBMIT, SUBMIT_DISABLED, SUBMIT_LOADING, DELETE, DELETE_LOADING, DELETE_DISABLED
|
||||
}
|
||||
|
||||
enum Event {
|
||||
NETWORK_FAILURE, SUBMIT_SUCCESS, DELETE_SUCCESS, SUBMIT_FAIL_INVALID, SUBMIT_FAIL_TAKEN
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new UsernameEditViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user