mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Implement new API endpoints for Usernames.
This commit is contained in:
committed by
Greyson Parrelli
parent
ca0e52e141
commit
9b9453734c
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
@@ -341,11 +342,11 @@ public class RetrieveProfileJob extends BaseJob {
|
|||||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||||
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||||
|
|
||||||
setProfileName(recipient, profile.getName());
|
boolean wroteNewProfileName = setProfileName(recipient, profile.getName());
|
||||||
|
|
||||||
setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji());
|
setProfileAbout(recipient, profile.getAbout(), profile.getAboutEmoji());
|
||||||
setProfileAvatar(recipient, profile.getAvatar());
|
setProfileAvatar(recipient, profile.getAvatar());
|
||||||
setProfileBadges(recipient, profile.getBadges());
|
setProfileBadges(recipient, profile.getBadges());
|
||||||
clearUsername(recipient);
|
|
||||||
setProfileCapabilities(recipient, profile.getCapabilities());
|
setProfileCapabilities(recipient, profile.getCapabilities());
|
||||||
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||||
|
|
||||||
@@ -353,6 +354,10 @@ public class RetrieveProfileJob extends BaseJob {
|
|||||||
profileAndCredential.getExpiringProfileKeyCredential()
|
profileAndCredential.getExpiringProfileKeyCredential()
|
||||||
.ifPresent(profileKeyCredential -> setExpiringProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential));
|
.ifPresent(profileKeyCredential -> setExpiringProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipient.hasNonUsernameDisplayName(context) || wroteNewProfileName) {
|
||||||
|
clearUsername(recipient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setProfileBadges(@NonNull Recipient recipient, @Nullable List<SignalServiceProfile.Badge> serviceBadges) {
|
private void setProfileBadges(@NonNull Recipient recipient, @Nullable List<SignalServiceProfile.Badge> serviceBadges) {
|
||||||
@@ -436,16 +441,16 @@ public class RetrieveProfileJob extends BaseJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setProfileName(Recipient recipient, String profileName) {
|
private boolean setProfileName(Recipient recipient, String profileName) {
|
||||||
try {
|
try {
|
||||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||||
if (profileKey == null) return;
|
if (profileKey == null) return false;
|
||||||
|
|
||||||
String plaintextProfileName = Util.emptyIfNull(ProfileUtil.decryptString(profileKey, profileName));
|
String plaintextProfileName = Util.emptyIfNull(ProfileUtil.decryptString(profileKey, profileName));
|
||||||
|
|
||||||
if (TextUtils.isEmpty(plaintextProfileName)) {
|
if (TextUtils.isEmpty(plaintextProfileName)) {
|
||||||
Log.w(TAG, "No name set on the profile for " + recipient.getId() + " -- Leaving it alone");
|
Log.w(TAG, "No name set on the profile for " + recipient.getId() + " -- Leaving it alone");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileName remoteProfileName = ProfileName.fromSerialized(plaintextProfileName);
|
ProfileName remoteProfileName = ProfileName.fromSerialized(plaintextProfileName);
|
||||||
@@ -470,12 +475,16 @@ public class RetrieveProfileJob extends BaseJob {
|
|||||||
Log.i(TAG, String.format(Locale.US, "Name changed, but wasn't relevant to write an event. blocked: %s, group: %s, self: %s, firstSet: %s, displayChange: %s",
|
Log.i(TAG, String.format(Locale.US, "Name changed, but wasn't relevant to write an event. blocked: %s, group: %s, self: %s, firstSet: %s, displayChange: %s",
|
||||||
recipient.isBlocked(), recipient.isGroup(), recipient.isSelf(), localDisplayName.isEmpty(), !remoteDisplayName.equals(localDisplayName)));
|
recipient.isBlocked(), recipient.isGroup(), recipient.isSelf(), localDisplayName.isEmpty(), !remoteDisplayName.equals(localDisplayName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
} catch (InvalidCiphertextException e) {
|
} catch (InvalidCiphertextException e) {
|
||||||
Log.w(TAG, "Bad profile key for " + recipient.getId());
|
Log.w(TAG, "Bad profile key for " + recipient.getId());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setProfileAbout(@NonNull Recipient recipient, @Nullable String encryptedAbout, @Nullable String encryptedEmoji) {
|
private void setProfileAbout(@NonNull Recipient recipient, @Nullable String encryptedAbout, @Nullable String encryptedEmoji) {
|
||||||
|
|||||||
+28
-20
@@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.profiles.manage;
|
package org.thoughtcrime.securesms.profiles.manage;
|
||||||
|
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -15,16 +16,15 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
|
||||||
import com.google.android.material.button.MaterialButton;
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec;
|
||||||
|
import com.google.android.material.progressindicator.IndeterminateDrawable;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
import org.signal.core.util.DimensionUnit;
|
import org.signal.core.util.DimensionUnit;
|
||||||
@@ -38,10 +38,8 @@ import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
|||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class UsernameEditFragment extends LoggingFragment {
|
public class UsernameEditFragment extends LoggingFragment {
|
||||||
|
|
||||||
@@ -76,19 +74,20 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
||||||
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
|
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
|
||||||
|
|
||||||
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted(binding.usernameText.getText().toString()));
|
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
|
||||||
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||||
|
|
||||||
binding.usernameText.setText(Recipient.self().getUsername().orElse(null));
|
UsernameState usernameState = Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE);
|
||||||
|
binding.usernameText.setText(usernameState.getNickname());
|
||||||
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void onTextChanged(String text) {
|
public void onTextChanged(@NonNull String text) {
|
||||||
viewModel.onUsernameUpdated(text);
|
viewModel.onNicknameUpdated(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
binding.usernameText.setOnEditorActionListener((v, actionId, event) -> {
|
binding.usernameText.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
viewModel.onUsernameSubmitted(binding.usernameText.getText().toString());
|
viewModel.onUsernameSubmitted();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -118,9 +117,10 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
|
|
||||||
layoutParams.topMargin = suffixTextView.getPaddingTop();
|
layoutParams.topMargin = suffixTextView.getPaddingTop();
|
||||||
layoutParams.bottomMargin = suffixTextView.getPaddingBottom();
|
layoutParams.bottomMargin = suffixTextView.getPaddingBottom();
|
||||||
|
layoutParams.setMarginEnd(suffixTextView.getPaddingEnd());
|
||||||
|
|
||||||
suffixProgress = new ImageView(requireContext());
|
suffixProgress = new ImageView(requireContext());
|
||||||
suffixProgress.setImageDrawable(UsernameSuffix.getInProgressDrawable(requireContext()));
|
suffixProgress.setImageDrawable(getInProgressDrawable());
|
||||||
suffixParent.addView(suffixProgress, 0, layoutParams);
|
suffixParent.addView(suffixProgress, 0, layoutParams);
|
||||||
|
|
||||||
suffixTextView.setOnClickListener(this::onLearnMore);
|
suffixTextView.setOnClickListener(this::onLearnMore);
|
||||||
@@ -148,7 +148,7 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||||
|
|
||||||
usernameInput.setEnabled(true);
|
usernameInput.setEnabled(true);
|
||||||
presentSuffix(state.getUsernameSuffix());
|
presentSuffix(state.getUsername());
|
||||||
|
|
||||||
switch (state.getButtonState()) {
|
switch (state.getButtonState()) {
|
||||||
case SUBMIT:
|
case SUBMIT:
|
||||||
@@ -224,18 +224,14 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
||||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||||
|
|
||||||
break;
|
|
||||||
case AVAILABLE:
|
|
||||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_available));
|
|
||||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_accent_green)));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void presentSuffix(@NonNull UsernameSuffix usernameSuffix) {
|
private void presentSuffix(@NonNull UsernameState usernameState) {
|
||||||
binding.usernameTextWrapper.setSuffixText(usernameSuffix.getCharSequence());
|
binding.usernameTextWrapper.setSuffixText(usernameState.getDiscriminator());
|
||||||
|
|
||||||
boolean isInProgress = usernameSuffix.isInProgress();
|
boolean isInProgress = usernameState.isInProgress();
|
||||||
|
|
||||||
if (isInProgress) {
|
if (isInProgress) {
|
||||||
suffixProgress.setVisibility(View.VISIBLE);
|
suffixProgress.setVisibility(View.VISIBLE);
|
||||||
@@ -244,11 +240,23 @@ public class UsernameEditFragment extends LoggingFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IndeterminateDrawable<CircularProgressIndicatorSpec> getInProgressDrawable() {
|
||||||
|
CircularProgressIndicatorSpec spec = new CircularProgressIndicatorSpec(requireContext(), null);
|
||||||
|
spec.indicatorInset = 0;
|
||||||
|
spec.indicatorSize = (int) DimensionUnit.DP.toPixels(16f);
|
||||||
|
spec.trackColor = ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant);
|
||||||
|
spec.trackThickness = (int) DimensionUnit.DP.toPixels(1f);
|
||||||
|
|
||||||
|
IndeterminateDrawable<CircularProgressIndicatorSpec> drawable = IndeterminateDrawable.createCircularDrawable(requireContext(), spec);
|
||||||
|
drawable.setBounds(0, 0, spec.indicatorSize, spec.indicatorSize);
|
||||||
|
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
|
||||||
private void onEvent(@NonNull UsernameEditViewModel.Event event) {
|
private void onEvent(@NonNull UsernameEditViewModel.Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case SUBMIT_SUCCESS:
|
case SUBMIT_SUCCESS:
|
||||||
ResultContract.setUsernameCreated(getParentFragmentManager());
|
ResultContract.setUsernameCreated(getParentFragmentManager());
|
||||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_successfully_set_username, Toast.LENGTH_SHORT).show();
|
|
||||||
NavHostFragment.findNavController(this).popBackStack();
|
NavHostFragment.findNavController(this).popBackStack();
|
||||||
break;
|
break;
|
||||||
case SUBMIT_FAIL_TAKEN:
|
case SUBMIT_FAIL_TAKEN:
|
||||||
|
|||||||
+35
-18
@@ -1,18 +1,19 @@
|
|||||||
package org.thoughtcrime.securesms.profiles.manage;
|
package org.thoughtcrime.securesms.profiles.manage;
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import org.signal.core.util.Result;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||||
|
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -21,18 +22,20 @@ class UsernameEditRepository {
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(UsernameEditRepository.class);
|
private static final String TAG = Log.tag(UsernameEditRepository.class);
|
||||||
|
|
||||||
private final Application application;
|
|
||||||
private final SignalServiceAccountManager accountManager;
|
private final SignalServiceAccountManager accountManager;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
|
|
||||||
UsernameEditRepository() {
|
UsernameEditRepository() {
|
||||||
this.application = ApplicationDependencies.getApplication();
|
|
||||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
this.executor = SignalExecutors.UNBOUNDED;
|
this.executor = SignalExecutors.UNBOUNDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setUsername(@NonNull String username, @NonNull Callback<UsernameSetResult> callback) {
|
void reserveUsername(@NonNull String nickname, @NonNull Callback<Result<ReserveUsernameResponse, UsernameSetResult>> callback) {
|
||||||
executor.execute(() -> callback.onComplete(setUsernameInternal(username)));
|
executor.execute(() -> callback.onComplete(reserveUsernameInternal(nickname)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse, @NonNull Callback<UsernameSetResult> callback) {
|
||||||
|
executor.execute(() -> callback.onComplete(confirmUsernameInternal(reserveUsernameResponse)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
|
void deleteUsername(@NonNull Callback<UsernameDeleteResult> callback) {
|
||||||
@@ -40,20 +43,38 @@ class UsernameEditRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private @NonNull UsernameSetResult setUsernameInternal(@NonNull String username) {
|
private @NonNull Result<ReserveUsernameResponse, UsernameSetResult> reserveUsernameInternal(@NonNull String nickname) {
|
||||||
try {
|
try {
|
||||||
accountManager.setUsername(username);
|
ReserveUsernameResponse username = accountManager.reserveUsername(nickname);
|
||||||
SignalDatabase.recipients().setUsername(Recipient.self().getId(), username);
|
Log.i(TAG, "[reserveUsername] Successfully reserved username.");
|
||||||
Log.i(TAG, "[setUsername] Successfully set username.");
|
return Result.success(username);
|
||||||
|
} catch (UsernameTakenException e) {
|
||||||
|
Log.w(TAG, "[reserveUsername] Username taken.");
|
||||||
|
return Result.failure(UsernameSetResult.USERNAME_UNAVAILABLE);
|
||||||
|
} catch (UsernameMalformedException e) {
|
||||||
|
Log.w(TAG, "[reserveUsername] Username malformed.");
|
||||||
|
return Result.failure(UsernameSetResult.USERNAME_INVALID);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "[reserveUsername] Generic network exception.", e);
|
||||||
|
return Result.failure(UsernameSetResult.NETWORK_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private @NonNull UsernameSetResult confirmUsernameInternal(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
|
||||||
|
try {
|
||||||
|
accountManager.confirmUsername(reserveUsernameResponse);
|
||||||
|
SignalDatabase.recipients().setUsername(Recipient.self().getId(), reserveUsernameResponse.getUsername());
|
||||||
|
Log.i(TAG, "[confirmUsername] Successfully reserved username.");
|
||||||
return UsernameSetResult.SUCCESS;
|
return UsernameSetResult.SUCCESS;
|
||||||
} catch (UsernameTakenException e) {
|
} catch (UsernameTakenException e) {
|
||||||
Log.w(TAG, "[setUsername] Username taken.");
|
Log.w(TAG, "[confirmUsername] Username gone.");
|
||||||
return UsernameSetResult.USERNAME_UNAVAILABLE;
|
return UsernameSetResult.USERNAME_UNAVAILABLE;
|
||||||
} catch (UsernameMalformedException e) {
|
} catch (UsernameIsNotReservedException e) {
|
||||||
Log.w(TAG, "[setUsername] Username malformed.");
|
Log.w(TAG, "[confirmUsername] Username was not reserved.");
|
||||||
return UsernameSetResult.USERNAME_INVALID;
|
return UsernameSetResult.USERNAME_INVALID;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, "[setUsername] Generic network exception.", e);
|
Log.w(TAG, "[confirmUsername] Generic network exception.", e);
|
||||||
return UsernameSetResult.NETWORK_ERROR;
|
return UsernameSetResult.NETWORK_ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,10 +100,6 @@ class UsernameEditRepository {
|
|||||||
SUCCESS, NETWORK_ERROR
|
SUCCESS, NETWORK_ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UsernameAvailableResult {
|
|
||||||
TRUE, FALSE, NETWORK_ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback<E> {
|
interface Callback<E> {
|
||||||
void onComplete(E result);
|
void onComplete(E result);
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-49
@@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.profiles.manage;
|
package org.thoughtcrime.securesms.profiles.manage;
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -9,86 +8,126 @@ import androidx.lifecycle.ViewModel;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
|
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
|
||||||
import org.thoughtcrime.securesms.util.rx.RxStore;
|
import org.thoughtcrime.securesms.util.rx.RxStore;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the state around username updates.
|
||||||
|
*
|
||||||
|
* A note on naming conventions:
|
||||||
|
*
|
||||||
|
* Usernames are made up of two discrete components, a nickname and a discriminator. They are formatted thusly:
|
||||||
|
*
|
||||||
|
* [nickname]#[discriminator]
|
||||||
|
*
|
||||||
|
* The nickname is user-controlled, whereas the discriminator is controlled by the server.
|
||||||
|
*/
|
||||||
class UsernameEditViewModel extends ViewModel {
|
class UsernameEditViewModel extends ViewModel {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(UsernameEditViewModel.class);
|
private static final long NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS = 500;
|
||||||
|
|
||||||
private final Application application;
|
private final SingleLiveEvent<Event> events;
|
||||||
private final SingleLiveEvent<Event> events;
|
private final UsernameEditRepository repo;
|
||||||
private final UsernameEditRepository repo;
|
private final RxStore<State> uiState;
|
||||||
private final RxStore<State> uiState;
|
private final PublishProcessor<String> nicknamePublisher;
|
||||||
|
private final CompositeDisposable disposables;
|
||||||
|
|
||||||
private UsernameEditViewModel() {
|
private UsernameEditViewModel() {
|
||||||
this.application = ApplicationDependencies.getApplication();
|
this.repo = new UsernameEditRepository();
|
||||||
this.repo = new UsernameEditRepository();
|
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, Recipient.self().getUsername().<UsernameState>map(UsernameState.Set::new).orElse(UsernameState.NoUsername.INSTANCE)), Schedulers.computation());
|
||||||
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameSuffix.NONE), Schedulers.computation());
|
this.events = new SingleLiveEvent<>();
|
||||||
this.events = new SingleLiveEvent<>();
|
this.nicknamePublisher = PublishProcessor.create();
|
||||||
|
this.disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
Disposable disposable = nicknamePublisher.debounce(NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribe(this::onNicknameChanged);
|
||||||
|
disposables.add(disposable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUsernameUpdated(@NonNull String username) {
|
@Override
|
||||||
|
protected void onCleared() {
|
||||||
|
super.onCleared();
|
||||||
|
disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onNicknameUpdated(@NonNull String nickname) {
|
||||||
uiState.update(state -> {
|
uiState.update(state -> {
|
||||||
if (TextUtils.isEmpty(username) && Recipient.self().getUsername().isPresent()) {
|
if (TextUtils.isEmpty(nickname) && Recipient.self().getUsername().isPresent()) {
|
||||||
return new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix);
|
return new State(ButtonState.DELETE, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.equals(Recipient.self().getUsername().orElse(null))) {
|
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(nickname);
|
||||||
return new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix);
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameState))
|
||||||
|
.orElseGet(() -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||||
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameSuffix))
|
|
||||||
.orElseGet(() -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
nicknamePublisher.onNext(nickname);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUsernameSubmitted(@NonNull String username) {
|
void onUsernameSubmitted() {
|
||||||
if (username.equals(Recipient.self().getUsername().orElse(null))) {
|
UsernameState usernameState = uiState.getState().getUsername();
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
|
||||||
|
if (!(usernameState instanceof UsernameState.Reserved)) {
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
if (Objects.equals(usernameState.getUsername(), Recipient.self().getUsername().orElse(null))) {
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(usernameState.getNickname());
|
||||||
|
|
||||||
if (invalidReason.isPresent()) {
|
if (invalidReason.isPresent()) {
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameState));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState));
|
||||||
|
|
||||||
repo.setUsername(username, (result) -> {
|
repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse(), (result) -> {
|
||||||
ThreadUtil.runOnMain(() -> {
|
ThreadUtil.runOnMain(() -> {
|
||||||
|
String nickname = usernameState.getNickname();
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case SUCCESS:
|
case SUCCESS:
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||||
events.postValue(Event.SUBMIT_SUCCESS);
|
events.postValue(Event.SUBMIT_SUCCESS);
|
||||||
break;
|
break;
|
||||||
case USERNAME_INVALID:
|
case USERNAME_INVALID:
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameState));
|
||||||
events.postValue(Event.SUBMIT_FAIL_INVALID);
|
events.postValue(Event.SUBMIT_FAIL_INVALID);
|
||||||
|
|
||||||
|
if (nickname != null) {
|
||||||
|
onNicknameUpdated(nickname);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case USERNAME_UNAVAILABLE:
|
case USERNAME_UNAVAILABLE:
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameState));
|
||||||
events.postValue(Event.SUBMIT_FAIL_TAKEN);
|
events.postValue(Event.SUBMIT_FAIL_TAKEN);
|
||||||
|
|
||||||
|
if (nickname != null) {
|
||||||
|
onNicknameUpdated(nickname);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case NETWORK_ERROR:
|
case NETWORK_ERROR:
|
||||||
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameState));
|
||||||
events.postValue(Event.NETWORK_FAILURE);
|
events.postValue(Event.NETWORK_FAILURE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -97,17 +136,17 @@ class UsernameEditViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onUsernameDeleted() {
|
void onUsernameDeleted() {
|
||||||
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState));
|
||||||
|
|
||||||
repo.deleteUsername((result) -> {
|
repo.deleteUsername((result) -> {
|
||||||
ThreadUtil.runOnMain(() -> {
|
ThreadUtil.runOnMain(() -> {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case SUCCESS:
|
case SUCCESS:
|
||||||
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState));
|
||||||
events.postValue(Event.DELETE_SUCCESS);
|
events.postValue(Event.DELETE_SUCCESS);
|
||||||
break;
|
break;
|
||||||
case NETWORK_ERROR:
|
case NETWORK_ERROR:
|
||||||
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix));
|
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState));
|
||||||
events.postValue(Event.NETWORK_FAILURE);
|
events.postValue(Event.NETWORK_FAILURE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,28 +162,68 @@ class UsernameEditViewModel extends ViewModel {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onNicknameChanged(@NonNull String nickname) {
|
||||||
|
if (TextUtils.isEmpty(nickname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE));
|
||||||
|
repo.reserveUsername(nickname, result -> {
|
||||||
|
ThreadUtil.runOnMain(() -> {
|
||||||
|
result.either(
|
||||||
|
reserveUsernameJsonResponse -> {
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse)));
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
failure -> {
|
||||||
|
switch (failure) {
|
||||||
|
case SUCCESS:
|
||||||
|
throw new AssertionError();
|
||||||
|
case USERNAME_INVALID:
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, UsernameState.NoUsername.INSTANCE));
|
||||||
|
break;
|
||||||
|
case USERNAME_UNAVAILABLE:
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
|
||||||
|
break;
|
||||||
|
case NETWORK_ERROR:
|
||||||
|
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
|
||||||
|
events.postValue(Event.NETWORK_FAILURE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {
|
private static UsernameStatus mapUsernameError(@NonNull InvalidReason invalidReason) {
|
||||||
switch (invalidReason) {
|
switch (invalidReason) {
|
||||||
case TOO_SHORT: return UsernameStatus.TOO_SHORT;
|
case TOO_SHORT:
|
||||||
case TOO_LONG: return UsernameStatus.TOO_LONG;
|
return UsernameStatus.TOO_SHORT;
|
||||||
case STARTS_WITH_NUMBER: return UsernameStatus.CANNOT_START_WITH_NUMBER;
|
case TOO_LONG:
|
||||||
case INVALID_CHARACTERS: return UsernameStatus.INVALID_CHARACTERS;
|
return UsernameStatus.TOO_LONG;
|
||||||
default: return UsernameStatus.INVALID_GENERIC;
|
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 {
|
static class State {
|
||||||
private final ButtonState buttonState;
|
private final ButtonState buttonState;
|
||||||
private final UsernameStatus usernameStatus;
|
private final UsernameStatus usernameStatus;
|
||||||
private final UsernameSuffix usernameSuffix;
|
private final UsernameState usernameState;
|
||||||
|
|
||||||
private State(@NonNull ButtonState buttonState,
|
private State(@NonNull ButtonState buttonState,
|
||||||
@NonNull UsernameStatus usernameStatus,
|
@NonNull UsernameStatus usernameStatus,
|
||||||
@NonNull UsernameSuffix usernameSuffix)
|
@NonNull UsernameState usernameState)
|
||||||
{
|
{
|
||||||
this.buttonState = buttonState;
|
this.buttonState = buttonState;
|
||||||
this.usernameStatus = usernameStatus;
|
this.usernameStatus = usernameStatus;
|
||||||
this.usernameSuffix = usernameSuffix;
|
this.usernameState = usernameState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull ButtonState getButtonState() {
|
@NonNull ButtonState getButtonState() {
|
||||||
@@ -155,13 +234,13 @@ class UsernameEditViewModel extends ViewModel {
|
|||||||
return usernameStatus;
|
return usernameStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull UsernameSuffix getUsernameSuffix() {
|
@NonNull UsernameState getUsername() {
|
||||||
return usernameSuffix;
|
return usernameState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UsernameStatus {
|
enum UsernameStatus {
|
||||||
NONE, AVAILABLE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
NONE, TAKEN, TOO_SHORT, TOO_LONG, CANNOT_START_WITH_NUMBER, INVALID_CHARACTERS, INVALID_GENERIC
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ButtonState {
|
enum ButtonState {
|
||||||
@@ -174,7 +253,7 @@ class UsernameEditViewModel extends ViewModel {
|
|||||||
|
|
||||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||||
@Override
|
@Override
|
||||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
return modelClass.cast(new UsernameEditViewModel());
|
return modelClass.cast(new UsernameEditViewModel());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.thoughtcrime.securesms.profiles.manage
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the state of the username suffix, which is a spanned CharSequence.
|
||||||
|
*/
|
||||||
|
sealed class UsernameState {
|
||||||
|
|
||||||
|
protected open val username: String? = null
|
||||||
|
open val isInProgress: Boolean = false
|
||||||
|
|
||||||
|
object Loading : UsernameState() {
|
||||||
|
override val isInProgress: Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
object NoUsername : UsernameState()
|
||||||
|
|
||||||
|
data class Reserved(
|
||||||
|
val reserveUsernameResponse: ReserveUsernameResponse
|
||||||
|
) : UsernameState() {
|
||||||
|
override val username: String? = reserveUsernameResponse.username
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Set(
|
||||||
|
override val username: String
|
||||||
|
) : UsernameState()
|
||||||
|
|
||||||
|
fun getNickname(): String? {
|
||||||
|
return username?.split('#')?.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDiscriminator(): String? {
|
||||||
|
return username?.split('#')?.lastOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.profiles.manage
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
|
|
||||||
import com.google.android.material.progressindicator.IndeterminateDrawable
|
|
||||||
import org.signal.core.util.DimensionUnit
|
|
||||||
import org.thoughtcrime.securesms.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describes the state of the username suffix, which is a spanned CharSequence.
|
|
||||||
*/
|
|
||||||
data class UsernameSuffix(
|
|
||||||
val charSequence: CharSequence?
|
|
||||||
) {
|
|
||||||
|
|
||||||
val isInProgress = charSequence == null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmField
|
|
||||||
val LOADING = UsernameSuffix(null)
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val NONE = UsernameSuffix("")
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun fromCode(code: Int) = UsernameSuffix("#$code")
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getInProgressDrawable(context: Context): IndeterminateDrawable<CircularProgressIndicatorSpec> {
|
|
||||||
val progressIndicatorSpec = CircularProgressIndicatorSpec(context, null).apply {
|
|
||||||
indicatorInset = 0
|
|
||||||
indicatorSize = DimensionUnit.DP.toPixels(16f).toInt()
|
|
||||||
trackColor = ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)
|
|
||||||
trackThickness = DimensionUnit.DP.toPixels(1f).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
return IndeterminateDrawable.createCircularDrawable(context, progressIndicatorSpec).apply {
|
|
||||||
setBounds(0, 0, DimensionUnit.DP.toPixels(16f).toInt(), DimensionUnit.DP.toPixels(16f).toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -570,6 +570,37 @@ public class Recipient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull String getDisplayName(@NonNull Context context) {
|
public @NonNull String getDisplayName(@NonNull Context context) {
|
||||||
|
String name = getNameFromLocalData(context);
|
||||||
|
|
||||||
|
if (Util.isEmpty(name)) {
|
||||||
|
name = context.getString(R.string.Recipient_unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringUtil.isolateBidi(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull String getDisplayNameOrUsername(@NonNull Context context) {
|
||||||
|
String name = getNameFromLocalData(context);
|
||||||
|
|
||||||
|
if (Util.isEmpty(name)) {
|
||||||
|
name = StringUtil.isolateBidi(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Util.isEmpty(name)) {
|
||||||
|
name = StringUtil.isolateBidi(context.getString(R.string.Recipient_unknown));
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringUtil.isolateBidi(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNonUsernameDisplayName(@NonNull Context context) {
|
||||||
|
return getNameFromLocalData(context) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return local name for user ignoring the username.
|
||||||
|
*/
|
||||||
|
private @Nullable String getNameFromLocalData(@NonNull Context context) {
|
||||||
String name = getGroupName(context);
|
String name = getGroupName(context);
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
if (Util.isEmpty(name)) {
|
||||||
@@ -588,40 +619,6 @@ public class Recipient {
|
|||||||
name = email;
|
name = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = context.getString(R.string.Recipient_unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringUtil.isolateBidi(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NonNull String getDisplayNameOrUsername(@NonNull Context context) {
|
|
||||||
String name = getGroupName(context);
|
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = systemContactName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = StringUtil.isolateBidi(getProfileName().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.isEmpty(name) && !Util.isEmpty(e164)) {
|
|
||||||
name = PhoneNumberFormatter.prettyPrint(e164);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = StringUtil.isolateBidi(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = StringUtil.isolateBidi(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.isEmpty(name)) {
|
|
||||||
name = StringUtil.isolateBidi(context.getString(R.string.Recipient_unknown));
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -67,8 +68,8 @@ public class UsernameUtil {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "No local user with this username. Searching remotely.");
|
Log.d(TAG, "No local user with this username. Searching remotely.");
|
||||||
SignalServiceProfile profile = ApplicationDependencies.getSignalServiceMessageReceiver().retrieveProfileByUsername(username, Optional.empty(), Locale.getDefault());
|
ACI aci = ApplicationDependencies.getSignalServiceAccountManager().getAciByUsername(username);
|
||||||
return Optional.ofNullable(profile.getServiceId());
|
return Optional.ofNullable(aci);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ sealed class Result<out S, out F> {
|
|||||||
data class Success<out S>(val success: S) : Result<S, Nothing>()
|
data class Success<out S>(val success: S) : Result<S, Nothing>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
fun <S> success(value: S) = Success(value)
|
fun <S> success(value: S) = Success(value)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun <F> failure(value: F) = Failure(value)
|
fun <F> failure(value: F) = Failure(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -70,6 +70,7 @@ import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
|||||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
||||||
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
|
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
|
||||||
|
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse;
|
||||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse;
|
import org.whispersystems.signalservice.internal.push.WhoAmIResponse;
|
||||||
@@ -866,8 +867,20 @@ public class SignalServiceAccountManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUsername(String username) throws IOException {
|
public ACI getAciByUsername(String username) throws IOException {
|
||||||
this.pushServiceSocket.setUsername(username);
|
return this.pushServiceSocket.getAciByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String nickname, String existingUsername) throws IOException {
|
||||||
|
this.pushServiceSocket.setUsername(nickname, existingUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReserveUsernameResponse reserveUsername(String nickname) throws IOException {
|
||||||
|
return this.pushServiceSocket.reserveUsername(nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void confirmUsername(ReserveUsernameResponse reserveUsernameResponse) throws IOException {
|
||||||
|
this.pushServiceSocket.confirmUsername(reserveUsernameResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
|
|||||||
-6
@@ -118,12 +118,6 @@ public class SignalServiceMessageReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
return socket.retrieveProfileByUsername(username, unidentifiedAccess, locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
|
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.signalservice.api.push.exceptions;
|
||||||
|
|
||||||
|
public class UsernameIsNotAssociatedWithAnAccountException extends NotFoundException {
|
||||||
|
public UsernameIsNotAssociatedWithAnAccountException() {
|
||||||
|
super("The given username is not associated with an account.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.signalservice.api.push.exceptions;
|
||||||
|
|
||||||
|
public class UsernameIsNotReservedException extends NonSuccessfulResponseCodeException {
|
||||||
|
public UsernameIsNotReservedException() {
|
||||||
|
super(409, "The given username is not associated with an account.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
class ConfirmUsernameRequest {
|
||||||
|
@JsonProperty
|
||||||
|
private String usernameToConfirm;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String reservationToken;
|
||||||
|
|
||||||
|
ConfirmUsernameRequest(String usernameToConfirm, String reservationToken) {
|
||||||
|
this.usernameToConfirm = usernameToConfirm;
|
||||||
|
this.reservationToken = reservationToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON POJO that represents the returned ACI from a call to
|
||||||
|
* /v1/account/username/[username]
|
||||||
|
*/
|
||||||
|
class GetAciByUsernameResponse {
|
||||||
|
@JsonProperty
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
GetAciByUsernameResponse() {}
|
||||||
|
|
||||||
|
String getUuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
-20
@@ -53,6 +53,7 @@ import org.whispersystems.signalservice.api.payments.CurrencyConversions;
|
|||||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||||
|
import org.whispersystems.signalservice.api.push.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
@@ -79,6 +80,8 @@ import org.whispersystems.signalservice.api.push.exceptions.RemoteAttestationRes
|
|||||||
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssociatedWithAnAccountException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
||||||
@@ -139,6 +142,8 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@@ -156,10 +161,12 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.annotations.NonNull;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
@@ -195,8 +202,10 @@ public class PushServiceSocket {
|
|||||||
private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
|
private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock";
|
||||||
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
|
private static final String REQUEST_PUSH_CHALLENGE = "/v1/accounts/fcm/preauth/%s/%s";
|
||||||
private static final String WHO_AM_I = "/v1/accounts/whoami";
|
private static final String WHO_AM_I = "/v1/accounts/whoami";
|
||||||
private static final String SET_USERNAME_PATH = "/v1/accounts/username/%s";
|
private static final String GET_USERNAME_PATH = "/v1/accounts/username/%s";
|
||||||
private static final String DELETE_USERNAME_PATH = "/v1/accounts/username";
|
private static final String MODIFY_USERNAME_PATH = "/v1/accounts/username";
|
||||||
|
private static final String RESERVE_USERNAME_PATH = "/v1/accounts/username/reserved";
|
||||||
|
private static final String CONFIRM_USERNAME_PATH = "/v1/accounts/username/confirm";
|
||||||
private static final String DELETE_ACCOUNT_PATH = "/v1/accounts/me";
|
private static final String DELETE_ACCOUNT_PATH = "/v1/accounts/me";
|
||||||
private static final String CHANGE_NUMBER_PATH = "/v1/accounts/number";
|
private static final String CHANGE_NUMBER_PATH = "/v1/accounts/number";
|
||||||
private static final String IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s";
|
private static final String IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s";
|
||||||
@@ -221,7 +230,6 @@ public class PushServiceSocket {
|
|||||||
private static final String PAYMENTS_AUTH_PATH = "/v1/payments/auth";
|
private static final String PAYMENTS_AUTH_PATH = "/v1/payments/auth";
|
||||||
|
|
||||||
private static final String PROFILE_PATH = "/v1/profile/%s";
|
private static final String PROFILE_PATH = "/v1/profile/%s";
|
||||||
private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s";
|
|
||||||
private static final String PROFILE_BATCH_CHECK_PATH = "/v1/profile/identity_check/batch";
|
private static final String PROFILE_BATCH_CHECK_PATH = "/v1/profile/identity_check/batch";
|
||||||
|
|
||||||
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery";
|
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery";
|
||||||
@@ -770,19 +778,6 @@ public class PushServiceSocket {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale)
|
|
||||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
|
||||||
{
|
|
||||||
String response = makeServiceRequest(String.format(PROFILE_USERNAME_PATH, username), "GET", null, AcceptLanguagesUtil.getHeadersWithAcceptLanguage(locale), unidentifiedAccess);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JsonUtil.fromJson(response, SignalServiceProfile.class);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
throw new MalformedResponseException("Unable to parse entity", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
|
public ListenableFuture<ProfileAndCredential> retrieveVersionedProfileAndCredential(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess, Locale locale) {
|
||||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target);
|
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target);
|
||||||
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target, profileKey);
|
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target, profileKey);
|
||||||
@@ -899,17 +894,102 @@ public class PushServiceSocket {
|
|||||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUsername(String username) throws IOException {
|
/**
|
||||||
makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, (responseCode, body) -> {
|
* Gets the ACI for the given username, if it exists. This is an unauthenticated request.
|
||||||
|
*
|
||||||
|
* This network request can have the following error responses:
|
||||||
|
* <ul>
|
||||||
|
* <li>404 - The username given is not associated with an account</li>
|
||||||
|
* <li>428 - Rate-limited, retry is available in the Retry-After header</li>
|
||||||
|
* <li>400 - Bad Request. The request included authentication.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param username The username to look up.
|
||||||
|
* @return The ACI for the given username if it exists.
|
||||||
|
* @throws IOException if a network exception occurs.
|
||||||
|
*/
|
||||||
|
public @NonNull ACI getAciByUsername(String username) throws IOException {
|
||||||
|
String response = makeServiceRequestWithoutAuthentication(
|
||||||
|
String.format(GET_USERNAME_PATH, URLEncoder.encode(username, StandardCharsets.UTF_8.toString())),
|
||||||
|
"GET",
|
||||||
|
null,
|
||||||
|
(responseCode, body) -> {
|
||||||
|
if (responseCode == 404) {
|
||||||
|
throw new UsernameIsNotAssociatedWithAnAccountException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
GetAciByUsernameResponse getAciByUsernameResponse = JsonUtil.fromJsonResponse(response, GetAciByUsernameResponse.class);
|
||||||
|
return ACI.from(UUID.fromString(getAciByUsernameResponse.getUuid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the username for the account without seeing the discriminator first.
|
||||||
|
*
|
||||||
|
* @param nickname The user-supplied nickname, which must meet the requirements for usernames.
|
||||||
|
* @param existingUsername (Optional) If the account has a current username, indicates what the client thinks the current username is. Allows the server to
|
||||||
|
* deduplicate repeated requests.
|
||||||
|
* @return The username as set by the server, which includes both the nickname and discriminator.
|
||||||
|
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
|
||||||
|
*/
|
||||||
|
public @NonNull String setUsername(@NonNull String nickname, @Nullable String existingUsername) throws IOException {
|
||||||
|
SetUsernameRequest setUsernameRequest = new SetUsernameRequest(nickname, existingUsername);
|
||||||
|
|
||||||
|
String responseString = makeServiceRequest(MODIFY_USERNAME_PATH, "PUT", JsonUtil.toJson(setUsernameRequest), NO_HEADERS, (responseCode, body) -> {
|
||||||
switch (responseCode) {
|
switch (responseCode) {
|
||||||
case 400: throw new UsernameMalformedException();
|
case 422: throw new UsernameMalformedException();
|
||||||
case 409: throw new UsernameTakenException();
|
case 409: throw new UsernameTakenException();
|
||||||
}
|
}
|
||||||
}, Optional.empty());
|
}, Optional.empty());
|
||||||
|
|
||||||
|
SetUsernameResponse response = JsonUtil.fromJsonResponse(responseString, SetUsernameResponse.class);
|
||||||
|
return response.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserve a username for the account. This replaces an existing reservation if one exists. The username is guaranteed to be available for 5 minutes and can
|
||||||
|
* be confirmed with confirmUsername.
|
||||||
|
*
|
||||||
|
* @param nickname The user-supplied nickname, which must meet the requirements for usernames.
|
||||||
|
* @return The reserved username. It is available for confirmation for 5 minutes.
|
||||||
|
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
|
||||||
|
*/
|
||||||
|
public @NonNull ReserveUsernameResponse reserveUsername(@NonNull String nickname) throws IOException {
|
||||||
|
ReserveUsernameRequest reserveUsernameRequest = new ReserveUsernameRequest(nickname);
|
||||||
|
|
||||||
|
String responseString = makeServiceRequest(RESERVE_USERNAME_PATH, "PUT", JsonUtil.toJson(reserveUsernameRequest), NO_HEADERS, (responseCode, body) -> {
|
||||||
|
switch (responseCode) {
|
||||||
|
case 422: throw new UsernameMalformedException();
|
||||||
|
case 409: throw new UsernameTakenException();
|
||||||
|
}
|
||||||
|
}, Optional.empty());
|
||||||
|
|
||||||
|
return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a previously reserved username for the account.
|
||||||
|
*
|
||||||
|
* @param reserveUsernameResponse The response object from the reservation
|
||||||
|
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
|
||||||
|
*/
|
||||||
|
public void confirmUsername(ReserveUsernameResponse reserveUsernameResponse) throws IOException {
|
||||||
|
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(reserveUsernameResponse.getUsername(), reserveUsernameResponse.getReservationToken());
|
||||||
|
|
||||||
|
makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body) -> {
|
||||||
|
switch (responseCode) {
|
||||||
|
case 409: throw new UsernameIsNotReservedException();
|
||||||
|
case 410: throw new UsernameTakenException();
|
||||||
|
}
|
||||||
|
}, Optional.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the username associated with the account.
|
||||||
|
*/
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
makeServiceRequest(DELETE_USERNAME_PATH, "DELETE", null);
|
makeServiceRequest(MODIFY_USERNAME_PATH, "DELETE", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteAccount() throws IOException {
|
public void deleteAccount() throws IOException {
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
class ReserveUsernameRequest {
|
||||||
|
@JsonProperty
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
ReserveUsernameRequest(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class ReserveUsernameResponse {
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String reservationToken;
|
||||||
|
|
||||||
|
ReserveUsernameResponse() {}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getReservationToken() {
|
||||||
|
return reservationToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
class SetUsernameRequest {
|
||||||
|
@JsonProperty
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String existingUsername;
|
||||||
|
|
||||||
|
SetUsernameRequest(String nickname, String existingUsername) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.existingUsername = existingUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getExistingUsername() {
|
||||||
|
return existingUsername;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package org.whispersystems.signalservice.internal.push;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
class SetUsernameResponse {
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
SetUsernameResponse() {}
|
||||||
|
|
||||||
|
String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user