mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-30 15:46:30 +00:00
Add support for setting an optional last name in profiles.
This commit is contained in:
committed by
Greyson Parrelli
parent
f2b9bf0b8c
commit
3907ec8b51
@@ -38,8 +38,8 @@ import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.usernames.ProfileEditActivityV2;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -266,14 +266,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (FeatureFlags.USERNAMES) {
|
||||
requireActivity().startActivity(ProfileEditActivityV2.getLaunchIntent(requireContext()));
|
||||
} else {
|
||||
Intent intent = new Intent(preference.getContext(), CreateProfileActivity.class);
|
||||
intent.putExtra(CreateProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
|
||||
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
|
||||
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
|
||||
|
||||
requireActivity().startActivity(intent);
|
||||
}
|
||||
requireActivity().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,447 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = CreateProfileActivity.class.getSimpleName();
|
||||
|
||||
public static final String NEXT_INTENT = "next_intent";
|
||||
public static final String EXCLUDE_SYSTEM = "exclude_system";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private InputAwareLayout container;
|
||||
private ImageView avatar;
|
||||
private CircularProgressButton finishButton;
|
||||
private LabeledEditText name;
|
||||
private EmojiToggle emojiToggle;
|
||||
private MediaKeyboard mediaKeyboard;
|
||||
private View reveal;
|
||||
|
||||
private Intent nextIntent;
|
||||
private byte[] avatarBytes;
|
||||
private File captureFile;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
|
||||
setContentView(R.layout.profile_create_activity);
|
||||
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
|
||||
initializeResources();
|
||||
initializeEmojiInput();
|
||||
initializeProfileName(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
|
||||
initializeProfileAvatar(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (container.isInputOpen()) container.hideCurrentInput(name.getInput());
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (container.getCurrentInput() == mediaKeyboard) {
|
||||
container.hideAttachedInput(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
|
||||
if (inputFile == null && captureFile != null) {
|
||||
inputFile = Uri.fromFile(captureFile);
|
||||
}
|
||||
|
||||
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||
avatarBytes = null;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
} else {
|
||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
new AsyncTask<Void, Void, byte[]>() {
|
||||
@Override
|
||||
protected byte[] doInBackground(Void... params) {
|
||||
try {
|
||||
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(CreateProfileActivity.this, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
|
||||
return result.getBitmap();
|
||||
} catch (BitmapDecodingException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(avatarBytes)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
} else {
|
||||
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
TextView skipButton = ViewUtil.findById(this, R.id.skip_button);
|
||||
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.name = ViewUtil.findById(this, R.id.name);
|
||||
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
|
||||
this.mediaKeyboard = ViewUtil.findById(this, R.id.emoji_drawer);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.finishButton = ViewUtil.findById(this, R.id.finish_button);
|
||||
this.reveal = ViewUtil.findById(this, R.id.reveal);
|
||||
this.nextIntent = getIntent().getParcelableExtra(NEXT_INTENT);
|
||||
|
||||
this.avatar.setOnClickListener(view -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(this::startAvatarSelection)
|
||||
.execute());
|
||||
|
||||
this.name.getInput().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) {
|
||||
name.getInput().setError(getString(R.string.CreateProfileActivity_too_long));
|
||||
finishButton.setEnabled(false);
|
||||
} else if (name.getInput().getError() != null || !finishButton.isEnabled()) {
|
||||
name.getInput().setError(null);
|
||||
finishButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.finishButton.setOnClickListener(view -> {
|
||||
this.finishButton.setIndeterminateProgressMode(true);
|
||||
this.finishButton.setProgress(50);
|
||||
handleUpload();
|
||||
});
|
||||
|
||||
skipButton.setOnClickListener(view -> {
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeProfileName(boolean excludeSystem) {
|
||||
if (!TextUtils.isEmpty(TextSecurePreferences.getProfileName(this))) {
|
||||
String profileName = TextSecurePreferences.getProfileName(this);
|
||||
|
||||
name.setText(profileName);
|
||||
name.getInput().setSelection(profileName.length(), profileName.length());
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileName(this).addListener(new ListenableFuture.Listener<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
if (!TextUtils.isEmpty(result)) {
|
||||
name.setText(result);
|
||||
name.getInput().setSelection(result.length(), result.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeProfileAvatar(boolean excludeSystem) {
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
if (AvatarHelper.getAvatarFile(this, selfId).exists() && AvatarHelper.getAvatarFile(this, selfId).length() > 0) {
|
||||
new AsyncTask<Void, Void, byte[]>() {
|
||||
@Override
|
||||
protected byte[] doInBackground(Void... params) {
|
||||
try {
|
||||
return Util.readFully(AvatarHelper.getInputStreamFor(CreateProfileActivity.this, selfId));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileAvatar(this, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
|
||||
@Override
|
||||
public void onSuccess(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeEmojiInput() {
|
||||
this.emojiToggle.attach(mediaKeyboard);
|
||||
|
||||
this.emojiToggle.setOnClickListener(v -> {
|
||||
if (container.getCurrentInput() == mediaKeyboard) {
|
||||
container.showSoftkey(name.getInput());
|
||||
} else {
|
||||
container.show(name.getInput(), mediaKeyboard);
|
||||
}
|
||||
});
|
||||
|
||||
this.mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
name.dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
final int start = name.getInput().getSelectionStart();
|
||||
final int end = name.getInput().getSelectionEnd();
|
||||
|
||||
name.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
|
||||
name.getInput().setSelection(start + emoji.length());
|
||||
}
|
||||
}));
|
||||
|
||||
this.container.addOnKeyboardShownListener(() -> emojiToggle.setToMedia());
|
||||
this.name.setOnClickListener(v -> container.showSoftkey(name.getInput()));
|
||||
}
|
||||
|
||||
private void startAvatarSelection() {
|
||||
captureFile = AvatarSelection.startAvatarSelection(this, avatarBytes != null, true);
|
||||
}
|
||||
|
||||
private void handleUpload() {
|
||||
final String name;
|
||||
final StreamDetails avatar;
|
||||
|
||||
if (TextUtils.isEmpty(this.name.getText().toString())) name = null;
|
||||
else name = this.name.getText().toString();
|
||||
|
||||
if (avatarBytes == null || avatarBytes.length == 0) avatar = null;
|
||||
else avatar = new StreamDetails(new ByteArrayInputStream(avatarBytes),
|
||||
"image/jpeg", avatarBytes.length);
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = CreateProfileActivity.this;
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
|
||||
try {
|
||||
accountManager.setProfileName(profileKey, name);
|
||||
TextSecurePreferences.setProfileName(context, name);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), name);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
accountManager.setProfileAvatar(profileKey, avatar);
|
||||
AvatarHelper.setAvatar(CreateProfileActivity.this, Recipient.self().getId(), avatarBytes);
|
||||
TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
if (result) {
|
||||
if (captureFile != null) captureFile.delete();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
|
||||
else handleFinishedLegacy();
|
||||
} else {
|
||||
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleFinishedLegacy() {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private void handleFinishedLollipop() {
|
||||
int[] finishButtonLocation = new int[2];
|
||||
int[] revealLocation = new int[2];
|
||||
|
||||
finishButton.getLocationInWindow(finishButtonLocation);
|
||||
reveal.getLocationInWindow(revealLocation);
|
||||
|
||||
int finishX = finishButtonLocation[0] - revealLocation[0];
|
||||
int finishY = finishButtonLocation[1] - revealLocation[1];
|
||||
|
||||
finishX += finishButton.getWidth() / 2;
|
||||
finishY += finishButton.getHeight() / 2;
|
||||
|
||||
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
|
||||
animation.setDuration(500);
|
||||
animation.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
});
|
||||
|
||||
reveal.setVisibility(View.VISIBLE);
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.view.View;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
@@ -71,7 +73,7 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity
|
||||
R.string.ExperienceUpgradeActivity_signal_profiles_are_here,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
|
||||
CreateProfileActivity.class,
|
||||
EditProfileActivity.class,
|
||||
false),
|
||||
READ_RECEIPTS(299,
|
||||
new IntroPage(0xFF2090EA,
|
||||
|
||||
@@ -5,13 +5,11 @@ import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -119,8 +117,8 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
|
||||
String name = recipient.toShortString(context);
|
||||
|
||||
if (recipient.getName(context) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
name += " ~" + recipient.getProfileName();
|
||||
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
|
||||
name += " ~" + recipient.getProfileName().toString();
|
||||
}
|
||||
|
||||
return name;
|
||||
|
||||
@@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
@@ -63,8 +61,8 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
} else if (!FeatureFlags.PROFILE_DISPLAY && recipient.getName(getContext()) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
|
||||
} else if (!FeatureFlags.PROFILE_DISPLAY && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
|
||||
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
|
||||
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -40,7 +39,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -384,8 +382,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientForeverObs
|
||||
} else {
|
||||
this.name.setText(recipient.getName(getContext()));
|
||||
|
||||
if (recipient.getName(getContext()) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
this.phoneNumber.setText(recipient.requireE164() + " (~" + recipient.getProfileName() + ")");
|
||||
if (recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
|
||||
this.phoneNumber.setText(recipient.requireE164() + " (~" + recipient.getProfileName().toString() + ")");
|
||||
} else {
|
||||
this.phoneNumber.setText(recipient.requireE164());
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ContactRepository {
|
||||
|
||||
add(new Pair<>(NAME_COLUMN, cursor -> {
|
||||
String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME));
|
||||
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SIGNAL_PROFILE_NAME));
|
||||
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SEARCH_PROFILE_NAME));
|
||||
|
||||
return Util.getFirstNonEmpty(system, profile);
|
||||
}));
|
||||
@@ -96,7 +96,7 @@ public class ContactRepository {
|
||||
boolean shouldAdd = !nameMatch && !numberMatch;
|
||||
|
||||
if (shouldAdd) {
|
||||
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION);
|
||||
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES);
|
||||
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle });
|
||||
|
||||
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package org.thoughtcrime.securesms.contacts.sync;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.StorageKeyDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
@@ -219,7 +214,7 @@ public final class StorageSyncHelper {
|
||||
|
||||
return new SignalContactRecord.Builder(storageKey, new SignalServiceAddress(recipient.getUuid(), recipient.getE164()))
|
||||
.setProfileKey(recipient.getProfileKey())
|
||||
.setProfileName(recipient.getProfileName())
|
||||
.setProfileName(recipient.getProfileName().serialize())
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setIdentityKey(recipient.getIdentityKey())
|
||||
|
||||
@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.util.cjkv.CJKVUtil;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.*;
|
||||
|
||||
public class ContactNameEditViewModel extends ViewModel {
|
||||
@@ -67,7 +69,12 @@ public class ContactNameEditViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
private String buildDisplayName() {
|
||||
boolean isCJKV = isCJKV(givenName) && isCJKV(middleName) && isCJKV(familyName) && isCJKV(prefix) && isCJKV(suffix);
|
||||
boolean isCJKV = CJKVUtil.isCJKV(givenName) &&
|
||||
CJKVUtil.isCJKV(middleName) &&
|
||||
CJKVUtil.isCJKV(familyName) &&
|
||||
CJKVUtil.isCJKV(prefix) &&
|
||||
CJKVUtil.isCJKV(suffix);
|
||||
|
||||
if (isCJKV) {
|
||||
return joinString(familyName, givenName, prefix, suffix, middleName);
|
||||
}
|
||||
@@ -86,47 +93,4 @@ public class ContactNameEditViewModel extends ViewModel {
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private boolean isCJKV(@Nullable String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int offset = 0; offset < value.length(); ) {
|
||||
int codepoint = Character.codePointAt(value, offset);
|
||||
|
||||
if (!isCodepointCJKV(codepoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += Character.charCount(codepoint);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isCodepointCJKV(int codepoint) {
|
||||
if (codepoint == (int)' ') return true;
|
||||
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
|
||||
|
||||
return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(block) ||
|
||||
Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS.equals(block) ||
|
||||
Character.UnicodeBlock.KANGXI_RADICALS.equals(block) ||
|
||||
Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS.equals(block) ||
|
||||
Character.UnicodeBlock.HIRAGANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_SYLLABLES.equals(block) ||
|
||||
Character.isIdeographic(codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,8 +966,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
} else {
|
||||
this.groupSender.setText(recipient.toShortString(context));
|
||||
|
||||
if (recipient.getName(context) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
this.groupSenderProfileName.setText("~" + recipient.getProfileName());
|
||||
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
|
||||
this.groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
|
||||
this.groupSenderProfileName.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.groupSenderProfileName.setText(null);
|
||||
|
||||
@@ -142,10 +142,10 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
private void setNonContactRecipientTitle(Recipient recipient) {
|
||||
this.title.setText(Util.getFirstNonEmpty(recipient.getE164().orNull(), recipient.getUuid().transform(UUID::toString).orNull()));
|
||||
|
||||
if (TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
if (recipient.getProfileName().isEmpty()) {
|
||||
this.subtitle.setText(null);
|
||||
} else {
|
||||
this.subtitle.setText("~" + recipient.getProfileName());
|
||||
this.subtitle.setText("~" + recipient.getProfileName().toString());
|
||||
}
|
||||
|
||||
updateSubtitleVisibility();
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@@ -79,7 +80,6 @@ public class RecipientDatabase extends Database {
|
||||
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
|
||||
private static final String SYSTEM_INFO_PENDING = "system_info_pending";
|
||||
private static final String PROFILE_KEY = "profile_key";
|
||||
public static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
|
||||
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
|
||||
private static final String PROFILE_SHARING = "profile_sharing";
|
||||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
@@ -87,7 +87,11 @@ public class RecipientDatabase extends Database {
|
||||
private static final String UUID_SUPPORTED = "uuid_supported";
|
||||
private static final String STORAGE_SERVICE_KEY = "storage_service_key";
|
||||
private static final String DIRTY = "dirty";
|
||||
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
|
||||
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
|
||||
private static final String PROFILE_JOINED_NAME = "profile_joined_name";
|
||||
|
||||
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
|
||||
private static final String SORT_NAME = "sort_name";
|
||||
private static final String IDENTITY_STATUS = "identity_status";
|
||||
private static final String IDENTITY_KEY = "identity_key";
|
||||
@@ -97,7 +101,7 @@ public class RecipientDatabase extends Database {
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION, UUID_SUPPORTED, STORAGE_SERVICE_KEY, DIRTY
|
||||
};
|
||||
@@ -116,7 +120,8 @@ public class RecipientDatabase extends Database {
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
public static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, SIGNAL_PROFILE_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + USERNAME + ") AS " + SORT_NAME};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + SYSTEM_DISPLAY_NAME + ", " + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ", " + USERNAME + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList();
|
||||
@@ -237,7 +242,9 @@ public class RecipientDatabase extends Database {
|
||||
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
|
||||
SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " +
|
||||
PROFILE_KEY + " TEXT DEFAULT NULL, " +
|
||||
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
|
||||
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
|
||||
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
|
||||
@@ -471,8 +478,12 @@ public class RecipientDatabase extends Database {
|
||||
values.put(UUID, contact.getAddress().getUuid().get().toString());
|
||||
}
|
||||
|
||||
ProfileName profileName = ProfileName.fromSerialized(contact.getProfileName().orNull());
|
||||
|
||||
values.put(PHONE, contact.getAddress().getNumber().orNull());
|
||||
values.put(SIGNAL_PROFILE_NAME, contact.getProfileName().orNull());
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, contact.getProfileKey().orNull());
|
||||
// TODO [greyson] Username
|
||||
values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0");
|
||||
@@ -551,7 +562,8 @@ public class RecipientDatabase extends Database {
|
||||
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
|
||||
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
|
||||
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
|
||||
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME));
|
||||
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
|
||||
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
|
||||
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
@@ -605,7 +617,7 @@ public class RecipientDatabase extends Database {
|
||||
RegisteredState.fromId(registeredState),
|
||||
profileKey, systemDisplayName, systemContactPhoto,
|
||||
systemPhoneLabel, systemContactUri,
|
||||
signalProfileName, signalProfileAvatar, profileSharing,
|
||||
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar, profileSharing,
|
||||
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection, uuidSupported, InsightsBannerTier.fromId(insightsBannerTier),
|
||||
storageKey, identityKey, identityStatus);
|
||||
@@ -751,9 +763,11 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public void setProfileName(@NonNull RecipientId id, @Nullable String profileName) {
|
||||
public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(SIGNAL_PROFILE_NAME, profileName);
|
||||
contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
contentValues.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
contentValues.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
@@ -1000,9 +1014,9 @@ public class RecipientDatabase extends Database {
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SIGNAL_PROFILE_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
||||
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SEARCH_PROFILE_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1" };
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
}
|
||||
@@ -1018,11 +1032,11 @@ public class RecipientDatabase extends Database {
|
||||
"(" +
|
||||
PHONE + " LIKE ? OR " +
|
||||
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
|
||||
SIGNAL_PROFILE_NAME + " LIKE ? OR " +
|
||||
SEARCH_PROFILE_NAME + " LIKE ? OR " +
|
||||
USERNAME + " LIKE ?" +
|
||||
")";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query, query };
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + PHONE;
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
}
|
||||
@@ -1066,7 +1080,7 @@ public class RecipientDatabase extends Database {
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
"(" +
|
||||
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
|
||||
SIGNAL_PROFILE_NAME + " LIKE ? OR " +
|
||||
SEARCH_PROFILE_NAME + " LIKE ? OR " +
|
||||
PHONE + " LIKE ? OR " +
|
||||
EMAIL + " LIKE ?" +
|
||||
")";
|
||||
@@ -1342,7 +1356,7 @@ public class RecipientDatabase extends Database {
|
||||
private final String systemContactPhoto;
|
||||
private final String systemPhoneLabel;
|
||||
private final String systemContactUri;
|
||||
private final String signalProfileName;
|
||||
private final ProfileName signalProfileName;
|
||||
private final String signalProfileAvatar;
|
||||
private final boolean profileSharing;
|
||||
private final String notificationChannel;
|
||||
@@ -1374,7 +1388,7 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String systemContactPhoto,
|
||||
@Nullable String systemPhoneLabel,
|
||||
@Nullable String systemContactUri,
|
||||
@Nullable String signalProfileName,
|
||||
@NonNull ProfileName signalProfileName,
|
||||
@Nullable String signalProfileAvatar,
|
||||
boolean profileSharing,
|
||||
@Nullable String notificationChannel,
|
||||
@@ -1508,7 +1522,7 @@ public class RecipientDatabase extends Database {
|
||||
return systemContactUri;
|
||||
}
|
||||
|
||||
public @Nullable String getProfileName() {
|
||||
public @NonNull ProfileName getProfileName() {
|
||||
return signalProfileName;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
@@ -20,7 +20,6 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteDatabaseHook;
|
||||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
@@ -47,7 +46,6 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
@@ -102,8 +100,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int RESUMABLE_DOWNLOADS = 40;
|
||||
private static final int KEY_VALUE_STORE = 41;
|
||||
private static final int ATTACHMENT_DISPLAY_ORDER = 42;
|
||||
private static final int SPLIT_PROFILE_NAMES = 43;
|
||||
|
||||
private static final int DATABASE_VERSION = 42;
|
||||
private static final int DATABASE_VERSION = 43;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -531,11 +530,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
values.put("phone", localNumber);
|
||||
values.put("registered", 1);
|
||||
values.put("profile_sharing", 1);
|
||||
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context));
|
||||
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context).getGivenName());
|
||||
db.insert("recipient", null, values);
|
||||
} else {
|
||||
db.execSQL("UPDATE recipient SET registered = ?, profile_sharing = ?, signal_profile_name = ? WHERE phone = ?",
|
||||
new String[] { "1", "1", TextSecurePreferences.getProfileName(context), localNumber });
|
||||
new String[] { "1", "1", TextSecurePreferences.getProfileName(context).getGivenName(), localNumber });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,6 +698,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN display_order INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < SPLIT_PROFILE_NAMES) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_family_name TEXT DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_joined_name TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
@@ -68,7 +67,7 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository
|
||||
public void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> avatarConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
Recipient self = Recipient.self().resolve();
|
||||
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context).toString())).or("");
|
||||
MaterialColor fallbackColor = self.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
|
||||
@@ -96,6 +96,7 @@ public final class JobManagerFactories {
|
||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
put(MarkerJob.KEY, new MarkerJob.Factory());
|
||||
put(Argon2TestJob.KEY, new Argon2TestJob.Factory());
|
||||
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
|
||||
|
||||
// Migrations
|
||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
public final class ProfileUploadJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "ProfileUploadJob";
|
||||
|
||||
private final Context context;
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
|
||||
public ProfileUploadJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(Parameters.IMMORTAL)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setMaxInstances(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
private ProfileUploadJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
uploadProfileName();
|
||||
uploadAvatar();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
private void uploadProfileName() throws Exception {
|
||||
ProfileName profileName = TextSecurePreferences.getProfileName(context);
|
||||
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(context), profileName.serialize());
|
||||
}
|
||||
|
||||
private void uploadAvatar() throws Exception {
|
||||
final RecipientId selfId = Recipient.self().getId();
|
||||
final byte[] avatar;
|
||||
|
||||
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
|
||||
avatar = Util.readFully(AvatarHelper.getInputStreamFor(context, Recipient.self().getId()));
|
||||
} else {
|
||||
avatar = null;
|
||||
}
|
||||
|
||||
final StreamDetails avatarDetails;
|
||||
if (avatar == null || avatar.length == 0) {
|
||||
avatarDetails = null;
|
||||
} else {
|
||||
avatarDetails = new StreamDetails(new ByteArrayInputStream(avatar),
|
||||
MediaUtil.IMAGE_JPEG,
|
||||
avatar.length);
|
||||
}
|
||||
|
||||
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(context), avatarDetails);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Job create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new ProfileUploadJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -74,11 +75,12 @@ public class RefreshOwnProfileJob extends BaseJob {
|
||||
|
||||
private void setProfileName(@Nullable String encryptedName) {
|
||||
try {
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
|
||||
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
|
||||
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
|
||||
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), plaintextName);
|
||||
TextSecurePreferences.setProfileName(context, plaintextName);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
|
||||
TextSecurePreferences.setProfileName(context, profileName);
|
||||
} catch (InvalidCiphertextException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
@@ -16,28 +14,19 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -191,9 +180,9 @@ public class RetrieveProfileJob extends BaseJob {
|
||||
|
||||
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);
|
||||
|
||||
if (!Util.equals(plaintextProfileName, recipient.getProfileName())) {
|
||||
if (!Util.equals(plaintextProfileName, recipient.getProfileName().serialize())) {
|
||||
Log.i(TAG, "Profile name updated. Writing new value.");
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), plaintextProfileName);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), ProfileName.fromSerialized(plaintextProfileName));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(plaintextProfileName)) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
@@ -52,7 +51,7 @@ public class RotateProfileKeyJob extends BaseJob {
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
byte[] profileKey = ProfileKeyUtil.rotateProfileKey(context);
|
||||
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context));
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
|
||||
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class ProfilePreference extends Preference {
|
||||
|
||||
@@ -66,7 +65,7 @@ public class ProfilePreference extends Preference {
|
||||
if (profileSubtextView == null) return;
|
||||
|
||||
final Recipient self = Recipient.self();
|
||||
final String profileName = TextSecurePreferences.getProfileName(getContext());
|
||||
final String profileName = TextSecurePreferences.getProfileName(getContext()).toString();
|
||||
|
||||
GlideApp.with(getContext().getApplicationContext())
|
||||
.load(new ProfileContactPhoto(self.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(getContext()))))
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package org.thoughtcrime.securesms.profiles;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.cjkv.CJKVUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class ProfileName implements Parcelable {
|
||||
|
||||
public static final ProfileName EMPTY = new ProfileName("", "");
|
||||
|
||||
private static final int MAX_PART_LENGTH = (ProfileCipher.NAME_PADDED_LENGTH - 1) / 2;
|
||||
|
||||
private final String givenName;
|
||||
private final String familyName;
|
||||
private final String joinedName;
|
||||
|
||||
private ProfileName(@Nullable String givenName, @Nullable String familyName) {
|
||||
this.givenName = sanitize(givenName);
|
||||
this.familyName = sanitize(familyName);
|
||||
this.joinedName = getJoinedName(this.givenName, this.familyName);
|
||||
}
|
||||
|
||||
private ProfileName(Parcel in) {
|
||||
this(in.readString(), in.readString());
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
String getGivenName() {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
String getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
public boolean isProfileNameCJKV() {
|
||||
return isCJKV(givenName, familyName);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return joinedName.isEmpty();
|
||||
}
|
||||
|
||||
public @NonNull String serialize() {
|
||||
if (isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String.format("%s\0%s", givenName, familyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return joinedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a profile name, trims if exceeds the limits.
|
||||
*/
|
||||
public static @NonNull ProfileName fromSerialized(@Nullable String profileName) {
|
||||
if (profileName == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
String[] parts = profileName.split("\0");
|
||||
|
||||
if (parts.length == 0) {
|
||||
return EMPTY;
|
||||
} else if (parts.length == 1) {
|
||||
return fromParts(parts[0], "");
|
||||
} else {
|
||||
return fromParts(parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a profile name, trimming chars until it fits the limits.
|
||||
*/
|
||||
public static @NonNull ProfileName fromParts(@Nullable String givenName, @Nullable String familyName) {
|
||||
if (givenName == null || givenName.isEmpty()) return EMPTY;
|
||||
|
||||
return new ProfileName(givenName, familyName);
|
||||
}
|
||||
|
||||
private static @NonNull String sanitize(@Nullable String name) {
|
||||
if (name == null) return "";
|
||||
|
||||
// At least one byte per char, so shorten string to reduce loop
|
||||
if (name.length() > ProfileName.MAX_PART_LENGTH) {
|
||||
name = name.substring(0, ProfileName.MAX_PART_LENGTH);
|
||||
}
|
||||
|
||||
// Remove one char at a time until fits in byte allowance
|
||||
while (name.getBytes(StandardCharsets.UTF_8).length > ProfileName.MAX_PART_LENGTH) {
|
||||
name = name.substring(0, name.length() - 1);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static @NonNull String getJoinedName(@NonNull String givenName, @NonNull String familyName) {
|
||||
if (givenName.isEmpty() && familyName.isEmpty()) return "";
|
||||
else if (givenName.isEmpty()) return familyName;
|
||||
else if (familyName.isEmpty()) return givenName;
|
||||
else if (isCJKV(givenName, familyName)) return String.format("%s %s",
|
||||
familyName,
|
||||
givenName);
|
||||
else return String.format("%s %s",
|
||||
givenName,
|
||||
familyName);
|
||||
}
|
||||
|
||||
private static boolean isCJKV(@NonNull String givenName, @NonNull String familyName) {
|
||||
if (givenName.isEmpty() && familyName.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
return Stream.of(givenName, familyName)
|
||||
.filterNot(String::isEmpty)
|
||||
.reduce(true, (a, s) -> a && CJKVUtil.isCJKV(s));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(givenName);
|
||||
dest.writeString(familyName);
|
||||
}
|
||||
|
||||
public static final Creator<ProfileName> CREATOR = new Creator<ProfileName>() {
|
||||
@Override
|
||||
public ProfileName createFromParcel(Parcel in) {
|
||||
return new ProfileName(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileName[] newArray(int size) {
|
||||
return new ProfileName[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class EditProfileActivity extends BaseActionBarActivity 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";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.profile_create_activity);
|
||||
|
||||
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
|
||||
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, getIntent().getExtras());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileNameUploadCompleted() {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Selection;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.NavDirections;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
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.NEXT_BUTTON_TEXT;
|
||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_INTENT;
|
||||
|
||||
public class EditProfileFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(EditProfileFragment.class);
|
||||
|
||||
private ImageView avatar;
|
||||
private CircularProgressButton finishButton;
|
||||
private EditText givenName;
|
||||
private EditText familyName;
|
||||
private View reveal;
|
||||
private TextView preview;
|
||||
private View usernameLabel;
|
||||
private View usernameEditButton;
|
||||
private TextView username;
|
||||
|
||||
private Intent nextIntent;
|
||||
private File captureFile;
|
||||
|
||||
private EditProfileViewModel viewModel;
|
||||
|
||||
private Controller controller;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (context instanceof Controller) {
|
||||
controller = (Controller) context;
|
||||
} else {
|
||||
throw new IllegalStateException("Context must subclass Controller");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return inflater.inflate(R.layout.profile_create_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeResources(view);
|
||||
initializeViewModel(getArguments().getBoolean(EXCLUDE_SYSTEM, false));
|
||||
initializeProfileName();
|
||||
initializeProfileAvatar();
|
||||
initializeUsername();
|
||||
|
||||
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Uri outputFile = Uri.fromFile(new File(requireActivity().getCacheDir(), "cropped"));
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
|
||||
if (inputFile == null && captureFile != null) {
|
||||
inputFile = Uri.fromFile(captureFile);
|
||||
}
|
||||
|
||||
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||
viewModel.setAvatar(null);
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
|
||||
} else {
|
||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
new AsyncTask<Void, Void, byte[]>() {
|
||||
@Override
|
||||
protected byte[] doInBackground(Void... params) {
|
||||
try {
|
||||
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(requireActivity(), AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
|
||||
return result.getBitmap();
|
||||
} catch (BitmapDecodingException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
viewModel.setAvatar(result);
|
||||
GlideApp.with(EditProfileFragment.this)
|
||||
.load(result)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
} else {
|
||||
Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeViewModel(boolean excludeSystem) {
|
||||
EditProfileRepository repository = new EditProfileRepository(requireContext(), excludeSystem);
|
||||
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(EditProfileViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeResources(@NonNull View view) {
|
||||
|
||||
this.avatar = view.findViewById(R.id.avatar);
|
||||
this.givenName = view.findViewById(R.id.given_name);
|
||||
this.familyName = view.findViewById(R.id.family_name);
|
||||
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 = getArguments().getParcelable(NEXT_INTENT);
|
||||
|
||||
if (FeatureFlags.USERNAMES && getArguments().getBoolean(DISPLAY_USERNAME, false)) {
|
||||
username.setVisibility(View.VISIBLE);
|
||||
usernameEditButton.setVisibility(View.VISIBLE);
|
||||
usernameLabel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
this.avatar.setOnClickListener(v -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(this::startAvatarSelection)
|
||||
.execute());
|
||||
|
||||
this.givenName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setGivenName(s.toString())));
|
||||
this.familyName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setFamilyName(s.toString())));
|
||||
|
||||
this.finishButton.setOnClickListener(v -> {
|
||||
this.finishButton.setIndeterminateProgressMode(true);
|
||||
this.finishButton.setProgress(50);
|
||||
handleUpload();
|
||||
});
|
||||
|
||||
this.finishButton.setText(getArguments().getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
|
||||
|
||||
this.usernameEditButton.setOnClickListener(v -> {
|
||||
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
|
||||
Navigation.findNavController(v).navigate(action);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeProfileName() {
|
||||
viewModel.profileName().observe(this, profileName -> {
|
||||
|
||||
updateFieldIfNeeded(givenName, profileName.getGivenName());
|
||||
updateFieldIfNeeded(familyName, profileName.getFamilyName());
|
||||
|
||||
finishButton.setEnabled(!profileName.isEmpty());
|
||||
finishButton.setAlpha(!profileName.isEmpty() ? 1f : 0.5f);
|
||||
|
||||
preview.setText(profileName.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeProfileAvatar() {
|
||||
viewModel.avatar().observe(this, bytes -> {
|
||||
if (bytes == null) return;
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(bytes)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeUsername() {
|
||||
viewModel.username().observe(this, this::onUsernameChanged);
|
||||
}
|
||||
|
||||
private void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) {
|
||||
if (!field.getText().toString().equals(value)) {
|
||||
|
||||
boolean setSelectionToEnd = field.getText().length() == 0;
|
||||
|
||||
field.setText(value);
|
||||
|
||||
if (setSelectionToEnd) {
|
||||
field.setSelection(field.getText().length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void onUsernameChanged(@NonNull Optional<String> username) {
|
||||
if (username.isPresent()) {
|
||||
this.username.setText("@" + username.get());
|
||||
} else {
|
||||
this.username.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private void startAvatarSelection() {
|
||||
captureFile = AvatarSelection.startAvatarSelection(this, viewModel.hasAvatar(), true);
|
||||
}
|
||||
|
||||
private void handleUpload() {
|
||||
|
||||
viewModel.submitProfile(uploadResult -> {
|
||||
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
|
||||
if (captureFile != null) captureFile.delete();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
|
||||
else handleFinishedLegacy();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleFinishedLegacy() {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
|
||||
controller.onProfileNameUploadCompleted();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private void handleFinishedLollipop() {
|
||||
int[] finishButtonLocation = new int[2];
|
||||
int[] revealLocation = new int[2];
|
||||
|
||||
finishButton.getLocationInWindow(finishButtonLocation);
|
||||
reveal.getLocationInWindow(revealLocation);
|
||||
|
||||
int finishX = finishButtonLocation[0] - revealLocation[0];
|
||||
int finishY = finishButtonLocation[1] - revealLocation[1];
|
||||
|
||||
finishX += finishButton.getWidth() / 2;
|
||||
finishY += finishButton.getHeight() / 2;
|
||||
|
||||
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
|
||||
animation.setDuration(500);
|
||||
animation.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
|
||||
controller.onProfileNameUploadCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
});
|
||||
|
||||
reveal.setVisibility(View.VISIBLE);
|
||||
animation.start();
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onProfileNameUploadCompleted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
class EditProfileRepository {
|
||||
|
||||
private static final String TAG = Log.tag(EditProfileRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final boolean excludeSystem;
|
||||
|
||||
EditProfileRepository(@NonNull Context context, boolean excludeSystem) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.excludeSystem = excludeSystem;
|
||||
}
|
||||
|
||||
void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer) {
|
||||
ProfileName storedProfileName = TextSecurePreferences.getProfileName(context);
|
||||
if (!storedProfileName.isEmpty()) {
|
||||
profileNameConsumer.accept(storedProfileName);
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
if (!TextUtils.isEmpty(result)) {
|
||||
profileNameConsumer.accept(ProfileName.fromSerialized(result));
|
||||
} else {
|
||||
profileNameConsumer.accept(storedProfileName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
profileNameConsumer.accept(storedProfileName);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
profileNameConsumer.accept(storedProfileName);
|
||||
}
|
||||
}
|
||||
|
||||
void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
return Util.readFully(AvatarHelper.getInputStreamFor(context, selfId));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}, avatarConsumer::accept);
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileAvatar(context, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
|
||||
@Override
|
||||
public void onSuccess(byte[] result) {
|
||||
avatarConsumer.accept(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
avatarConsumer.accept(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void uploadProfile(@NonNull ProfileName profileName, @Nullable byte[] avatar, @NonNull Consumer<UploadResult> uploadResultConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
TextSecurePreferences.setProfileName(context, profileName);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
|
||||
|
||||
try {
|
||||
AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar);
|
||||
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
return UploadResult.ERROR_FILE_IO;
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(new ProfileUploadJob())
|
||||
.then(Arrays.asList(new MultiDeviceProfileKeyUpdateJob(), new MultiDeviceProfileContentUpdateJob()))
|
||||
.enqueue();
|
||||
|
||||
return UploadResult.SUCCESS;
|
||||
}, uploadResultConsumer::accept);
|
||||
}
|
||||
|
||||
void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
|
||||
callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context)));
|
||||
SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<String> getUsernameInternal() {
|
||||
try {
|
||||
SignalServiceProfile profile = retrieveOwnProfile();
|
||||
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
|
||||
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
|
||||
}
|
||||
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveOwnProfile() throws IOException {
|
||||
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(context), TextSecurePreferences.getLocalNumber(context));
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(address, Optional.absent());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return receiver.retrieveProfile(address, Optional.absent());
|
||||
}
|
||||
|
||||
public enum UploadResult {
|
||||
SUCCESS,
|
||||
ERROR_FILE_IO
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
class EditProfileViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> givenName = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> familyName = new MutableLiveData<>();
|
||||
private final LiveData<ProfileName> internalProfileName = Transformations.map(new LiveDataPair<>(givenName, familyName),
|
||||
pair -> ProfileName.fromParts(pair.first(), pair.second()));
|
||||
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
|
||||
private final EditProfileRepository repository;
|
||||
|
||||
private EditProfileViewModel(@NonNull EditProfileRepository repository) {
|
||||
this.repository = repository;
|
||||
|
||||
repository.getCurrentUsername(internalUsername::postValue);
|
||||
repository.getCurrentProfileName(name -> {
|
||||
givenName.setValue(name.getGivenName());
|
||||
familyName.setValue(name.getFamilyName());
|
||||
});
|
||||
repository.getCurrentAvatar(internalAvatar::setValue);
|
||||
}
|
||||
|
||||
public LiveData<ProfileName> profileName() {
|
||||
return internalProfileName;
|
||||
}
|
||||
|
||||
public LiveData<byte[]> avatar() {
|
||||
return Transformations.distinctUntilChanged(internalAvatar);
|
||||
}
|
||||
|
||||
public LiveData<Optional<String>> username() {
|
||||
return internalUsername;
|
||||
}
|
||||
|
||||
public boolean hasAvatar() {
|
||||
return internalAvatar.getValue() != null;
|
||||
}
|
||||
|
||||
public void setGivenName(String givenName) {
|
||||
this.givenName.setValue(givenName);
|
||||
}
|
||||
|
||||
public void setFamilyName(String familyName) {
|
||||
this.familyName.setValue(familyName);
|
||||
}
|
||||
|
||||
public void setAvatar(byte[] avatar) {
|
||||
internalAvatar.setValue(avatar);
|
||||
}
|
||||
|
||||
public void submitProfile(Consumer<EditProfileRepository.UploadResult> uploadResultConsumer) {
|
||||
ProfileName profileName = internalProfileName.getValue();
|
||||
if (profileName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.uploadProfile(profileName, internalAvatar.getValue(), uploadResultConsumer);
|
||||
}
|
||||
|
||||
private ProfileName currentProfileName() {
|
||||
return internalProfileName.getValue();
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final EditProfileRepository repository;
|
||||
|
||||
Factory(EditProfileRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new EditProfileViewModel(repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
@@ -34,9 +33,9 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
@@ -83,7 +82,7 @@ public class Recipient {
|
||||
private final Uri systemContactPhoto;
|
||||
private final String customLabel;
|
||||
private final Uri contactUri;
|
||||
private final String profileName;
|
||||
private final ProfileName profileName;
|
||||
private final String profileAvatar;
|
||||
private final boolean profileSharing;
|
||||
private final String notificationChannel;
|
||||
@@ -295,7 +294,7 @@ public class Recipient {
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
this.contactUri = null;
|
||||
this.profileName = null;
|
||||
this.profileName = ProfileName.EMPTY;
|
||||
this.profileAvatar = null;
|
||||
this.profileSharing = false;
|
||||
this.notificationChannel = null;
|
||||
@@ -383,7 +382,7 @@ public class Recipient {
|
||||
|
||||
public @NonNull String getDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName(),
|
||||
getProfileName().toString(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
@@ -518,7 +517,7 @@ public class Recipient {
|
||||
return defaultSubscriptionId;
|
||||
}
|
||||
|
||||
public @Nullable String getProfileName() {
|
||||
public @NonNull ProfileName getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -43,7 +44,7 @@ public class RecipientDetails {
|
||||
final boolean blocked;
|
||||
final int expireMessages;
|
||||
final List<Recipient> participants;
|
||||
final String profileName;
|
||||
final ProfileName profileName;
|
||||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
|
||||
@@ -21,7 +21,8 @@ public final class RecipientExporter {
|
||||
public Intent asAddContactIntent() {
|
||||
Intent intent = new Intent(ACTION_INSERT_OR_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
addNameToIntent(intent, recipient.getProfileName());
|
||||
|
||||
addNameToIntent(intent, recipient.getProfileName().toString());
|
||||
addAddressToIntent(intent, recipient);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
|
||||
import org.thoughtcrime.securesms.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
|
||||
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
||||
|
||||
@@ -31,8 +31,7 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
||||
if (!isReregister()) {
|
||||
// TODO [greyson] Navigation
|
||||
activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, MainActivity.class)));
|
||||
activity.startActivity(getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class)));
|
||||
}
|
||||
|
||||
activity.finish();
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.navigation.ActivityNavigator;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavDestination;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.ui.AppBarConfiguration;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class ProfileEditActivityV2 extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
public static Intent getLaunchIntent(@NonNull Context context) {
|
||||
return new Intent(context, ProfileEditActivityV2.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
setContentView(R.layout.profile_edit_activity_v2);
|
||||
initToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
|
||||
|
||||
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
|
||||
getSupportActionBar().setTitle(destination.getLabel());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class ProfileEditOverviewFragment extends Fragment {
|
||||
|
||||
private ImageView avatarView;
|
||||
private TextView profileText;
|
||||
private TextView usernameText;
|
||||
private AlertDialog loadingDialog;
|
||||
|
||||
private ProfileEditOverviewViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.profile_edit_overview_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
avatarView = view.findViewById(R.id.profile_overview_avatar);
|
||||
profileText = view.findViewById(R.id.profile_overview_profile_name);
|
||||
usernameText = view.findViewById(R.id.profile_overview_username);
|
||||
|
||||
View profileButton = view.findViewById(R.id.profile_overview_profile_edit_button );
|
||||
View usernameButton = view.findViewById(R.id.profile_overview_username_edit_button);
|
||||
TextView infoText = view.findViewById(R.id.profile_overview_info_text);
|
||||
|
||||
profileButton.setOnClickListener(v -> {
|
||||
Navigation.findNavController(view).navigate(ProfileEditOverviewFragmentDirections.actionProfileEdit());
|
||||
});
|
||||
|
||||
usernameButton.setOnClickListener(v -> {
|
||||
Navigation.findNavController(view).navigate(ProfileEditOverviewFragmentDirections.actionUsernameEdit());
|
||||
});
|
||||
|
||||
infoText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
profileText.setOnClickListener(v -> profileButton.callOnClick());
|
||||
usernameText.setOnClickListener(v -> usernameButton.callOnClick());
|
||||
|
||||
avatarView.setOnClickListener(v -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(() -> viewModel.onAvatarClicked(this))
|
||||
.execute());
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new ProfileEditOverviewViewModel.Factory()).get(ProfileEditOverviewViewModel.class);
|
||||
viewModel.getAvatar().observe(getViewLifecycleOwner(), this::onAvatarChanged);
|
||||
viewModel.getLoading().observe(getViewLifecycleOwner(), this::onLoadingChanged);
|
||||
viewModel.getProfileName().observe(getViewLifecycleOwner(), this::onProfileNameChanged);
|
||||
viewModel.getUsername().observe(getViewLifecycleOwner(), this::onUsernameChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (!viewModel.onActivityResult(this, requestCode, resultCode, data)) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
viewModel.onResume();
|
||||
}
|
||||
|
||||
private void onAvatarChanged(@NonNull Optional<byte[]> avatar) {
|
||||
if (avatar.isPresent()) {
|
||||
GlideApp.with(this)
|
||||
.load(avatar.get())
|
||||
.circleCrop()
|
||||
.into(avatarView);
|
||||
} else {
|
||||
avatarView.setImageDrawable(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoadingChanged(boolean loading) {
|
||||
if (loadingDialog == null && loading) {
|
||||
loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
} else if (loadingDialog != null) {
|
||||
loadingDialog.dismiss();
|
||||
loadingDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onProfileNameChanged(@NonNull Optional<String> profileName) {
|
||||
profileText.setText(profileName.or(""));
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void onUsernameChanged(@NonNull Optional<String> username) {
|
||||
if (username.isPresent()) {
|
||||
usernameText.setText("@" + username.get());
|
||||
} else {
|
||||
usernameText.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.w3c.dom.Text;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class ProfileEditOverviewRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ProfileEditOverviewRepository.class);
|
||||
|
||||
private final Application application;
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final Executor executor;
|
||||
|
||||
ProfileEditOverviewRepository() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
this.executor = SignalExecutors.UNBOUNDED;
|
||||
}
|
||||
|
||||
void getProfileAvatar(@NonNull Callback<Optional<byte[]>> callback) {
|
||||
executor.execute(() -> callback.onResult(getProfileAvatarInternal()));
|
||||
}
|
||||
|
||||
void setProfileAvatar(@NonNull byte[] data, @NonNull Callback<ProfileAvatarResult> callback) {
|
||||
executor.execute(() -> callback.onResult(setProfileAvatarInternal(data)));
|
||||
}
|
||||
|
||||
void deleteProfileAvatar(@NonNull Callback<ProfileAvatarResult> callback) {
|
||||
executor.execute(() -> callback.onResult(deleteProfileAvatarInternal()));
|
||||
}
|
||||
|
||||
void getProfileName(@NonNull Callback<Optional<String>> callback) {
|
||||
executor.execute(() -> callback.onResult(getProfileNameInternal()));
|
||||
}
|
||||
|
||||
void getUsername(@NonNull Callback<Optional<String>> callback) {
|
||||
executor.execute(() -> callback.onResult(getUsernameInternal()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<byte[]> getProfileAvatarInternal() {
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
if (AvatarHelper.getAvatarFile(application, selfId).exists() && AvatarHelper.getAvatarFile(application, selfId).length() > 0) {
|
||||
try {
|
||||
return Optional.of(Util.readFully(AvatarHelper.getInputStreamFor(application, selfId)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar!", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ProfileAvatarResult setProfileAvatarInternal(@NonNull byte[] data) {
|
||||
StreamDetails avatar = new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length);
|
||||
try {
|
||||
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(application), avatar);
|
||||
AvatarHelper.setAvatar(application, Recipient.self().getId(), data);
|
||||
TextSecurePreferences.setProfileAvatarId(application, new SecureRandom().nextInt());
|
||||
return ProfileAvatarResult.SUCCESS;
|
||||
} catch (IOException e) {
|
||||
return ProfileAvatarResult.NETWORK_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ProfileAvatarResult deleteProfileAvatarInternal() {
|
||||
try {
|
||||
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(application), null);
|
||||
AvatarHelper.delete(application, Recipient.self().getId());
|
||||
TextSecurePreferences.setProfileAvatarId(application, 0);
|
||||
return ProfileAvatarResult.SUCCESS;
|
||||
} catch (IOException e) {
|
||||
return ProfileAvatarResult.NETWORK_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<String> getProfileNameInternal() {
|
||||
try {
|
||||
SignalServiceProfile profile = retrieveOwnProfile();
|
||||
String encryptedProfileName = profile.getName();
|
||||
String plaintextProfileName = null;
|
||||
|
||||
if (encryptedProfileName != null) {
|
||||
ProfileCipher profileCipher = new ProfileCipher(ProfileKeyUtil.getProfileKey(application));
|
||||
plaintextProfileName = new String(profileCipher.decryptName(Base64.decode(encryptedProfileName)));
|
||||
}
|
||||
|
||||
TextSecurePreferences.setProfileName(application, plaintextProfileName);
|
||||
DatabaseFactory.getRecipientDatabase(application).setProfileName(Recipient.self().getId(), plaintextProfileName);
|
||||
} catch (IOException | InvalidCiphertextException e) {
|
||||
Log.w(TAG, "Failed to retrieve profile name remotely! Using locally-cached version.");
|
||||
}
|
||||
|
||||
return Optional.fromNullable(TextSecurePreferences.getProfileName(application));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<String> getUsernameInternal() {
|
||||
try {
|
||||
SignalServiceProfile profile = retrieveOwnProfile();
|
||||
TextSecurePreferences.setLocalUsername(application, profile.getUsername());
|
||||
DatabaseFactory.getRecipientDatabase(application).setUsername(Recipient.self().getId(), profile.getUsername());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
|
||||
}
|
||||
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(application));
|
||||
}
|
||||
|
||||
|
||||
private SignalServiceProfile retrieveOwnProfile() throws IOException {
|
||||
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(application), TextSecurePreferences.getLocalNumber(application));
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(address, Optional.absent());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return receiver.retrieveProfile(address, Optional.absent());
|
||||
}
|
||||
|
||||
enum ProfileAvatarResult {
|
||||
SUCCESS, NETWORK_FAILURE
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onResult(@NonNull E result);
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class ProfileEditOverviewViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ProfileEditOverviewViewModel.class);
|
||||
|
||||
private final Application application;
|
||||
private final ProfileEditOverviewRepository repo;
|
||||
private final SingleLiveEvent<Event> event;
|
||||
private final MutableLiveData<Optional<byte[]>> avatar;
|
||||
private final MutableLiveData<Boolean> loading;
|
||||
private final MutableLiveData<Optional<String>> profileName;
|
||||
private final MutableLiveData<Optional<String>> username;
|
||||
|
||||
private File captureFile;
|
||||
|
||||
private ProfileEditOverviewViewModel() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.repo = new ProfileEditOverviewRepository();
|
||||
this.avatar = new MutableLiveData<>();
|
||||
this.loading = new MutableLiveData<>();
|
||||
this.profileName = new MutableLiveData<>();
|
||||
this.username = new MutableLiveData<>();
|
||||
this.event = new SingleLiveEvent<>();
|
||||
|
||||
profileName.setValue(Optional.fromNullable(TextSecurePreferences.getProfileName(application)));
|
||||
username.setValue(Optional.fromNullable(TextSecurePreferences.getLocalUsername(application)));
|
||||
loading.setValue(false);
|
||||
|
||||
repo.getProfileAvatar(avatar::postValue);
|
||||
repo.getProfileName(profileName::postValue);
|
||||
repo.getUsername(username::postValue);
|
||||
}
|
||||
|
||||
void onAvatarClicked(@NonNull Fragment fragment) {
|
||||
//noinspection ConstantConditions Initial value is set
|
||||
captureFile = AvatarSelection.startAvatarSelection(fragment, avatar.getValue().isPresent(), true);
|
||||
}
|
||||
|
||||
boolean onActivityResult(@NonNull Fragment fragment, int requestCode, int resultCode, @Nullable Intent data) {
|
||||
switch (requestCode) {
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
handleAvatarResult(fragment, resultCode, data);
|
||||
return true;
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
handleCropImage(resultCode, data);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void onResume() {
|
||||
profileName.setValue(Optional.fromNullable(TextSecurePreferences.getProfileName(application)));
|
||||
username.setValue(Optional.fromNullable(TextSecurePreferences.getLocalUsername(application)));
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<byte[]>> getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getLoading() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<String>> getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<String>> getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return event;
|
||||
}
|
||||
|
||||
private void handleAvatarResult(@NonNull Fragment fragment, int resultCode, @Nullable Intent data) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
Log.w(TAG, "Bad result for REQUEST_CODE_AVATAR.");
|
||||
event.postValue(Event.IMAGE_SAVE_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||
Log.i(TAG, "Deleting profile avatar.");
|
||||
|
||||
Optional<byte[]> oldAvatar = avatar.getValue();
|
||||
|
||||
avatar.setValue(Optional.absent());
|
||||
loading.setValue(true);
|
||||
|
||||
repo.deleteProfileAvatar(result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
loading.postValue(false);
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
loading.postValue(false);
|
||||
avatar.postValue(oldAvatar);
|
||||
event.postValue(Event.NETWORK_ERROR);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Uri outputFile = Uri.fromFile(new File(application.getCacheDir(), "cropped"));
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
|
||||
if (inputFile == null && captureFile != null) {
|
||||
inputFile = Uri.fromFile(captureFile);
|
||||
}
|
||||
|
||||
if (inputFile != null) {
|
||||
AvatarSelection.circularCropImage(fragment, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
|
||||
} else {
|
||||
Log.w(TAG, "No input file!");
|
||||
event.postValue(Event.IMAGE_SAVE_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCropImage(int resultCode, @Nullable Intent data) {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
Log.w(TAG, "Bad result for REQUEST_CODE_CROP_IMAGE.");
|
||||
event.postValue(Event.IMAGE_SAVE_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<byte[]> oldAvatar = avatar.getValue();
|
||||
|
||||
loading.setValue(true);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
BitmapUtil.ScaleResult scaled = BitmapUtil.createScaledBytes(application, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
|
||||
|
||||
if (captureFile != null) {
|
||||
captureFile.delete();
|
||||
}
|
||||
|
||||
avatar.postValue(Optional.of(scaled.getBitmap()));
|
||||
|
||||
repo.setProfileAvatar(scaled.getBitmap(), result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
loading.postValue(false);
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
loading.postValue(false);
|
||||
avatar.postValue(oldAvatar);
|
||||
event.postValue(Event.NETWORK_ERROR);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch (BitmapDecodingException e) {
|
||||
event.postValue(Event.IMAGE_SAVE_FAILURE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (captureFile != null) {
|
||||
captureFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
IMAGE_SAVE_FAILURE, NETWORK_ERROR
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ProfileEditOverviewViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames.profile;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class ProfileEditNameFragment extends Fragment {
|
||||
|
||||
private EditText profileText;
|
||||
private CircularProgressButton submitButton;
|
||||
private ProfileEditNameViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.profile_edit_name_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
profileText = view.findViewById(R.id.profile_name_text);
|
||||
submitButton = view.findViewById(R.id.profile_name_submit);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new ProfileEditNameViewModel.Factory()).get(ProfileEditNameViewModel.class);
|
||||
|
||||
viewModel.isLoading().observe(getViewLifecycleOwner(), this::onLoadingChanged);
|
||||
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
|
||||
|
||||
profileText.setText(TextSecurePreferences.getProfileName(requireContext()));
|
||||
submitButton.setOnClickListener(v -> viewModel.onSubmitPressed(profileText.getText().toString()));
|
||||
}
|
||||
|
||||
private void onLoadingChanged(boolean loading) {
|
||||
if (loading) {
|
||||
profileText.setEnabled(false);
|
||||
setSpinning(submitButton);
|
||||
} else {
|
||||
profileText.setEnabled(true);
|
||||
cancelSpinning(submitButton);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEvent(@NonNull ProfileEditNameViewModel.Event event) {
|
||||
switch (event) {
|
||||
case SUCCESS:
|
||||
Toast.makeText(requireContext(), R.string.ProfileEditNameFragment_successfully_set_profile_name, Toast.LENGTH_SHORT).show();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
Toast.makeText(requireContext(), R.string.ProfileEditNameFragment_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);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames.profile;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class ProfileEditNameRepository {
|
||||
|
||||
private final Application application;
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final Executor executor;
|
||||
|
||||
ProfileEditNameRepository() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
this.executor = SignalExecutors.UNBOUNDED;
|
||||
}
|
||||
|
||||
void setProfileName(@NonNull String profileName, @NonNull Callback<ProfileNameResult> callback) {
|
||||
executor.execute(() -> callback.onResult(setProfileNameInternal(profileName)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ProfileNameResult setProfileNameInternal(@NonNull String profileName) {
|
||||
Util.sleep(1000);
|
||||
try {
|
||||
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(application), profileName);
|
||||
TextSecurePreferences.setProfileName(application, profileName);
|
||||
DatabaseFactory.getRecipientDatabase(application).setProfileName(Recipient.self().getId(), profileName);
|
||||
return ProfileNameResult.SUCCESS;
|
||||
} catch (IOException e) {
|
||||
return ProfileNameResult.NETWORK_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
enum ProfileNameResult {
|
||||
SUCCESS, NETWORK_FAILURE
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onResult(@NonNull E result);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.usernames.profile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
class ProfileEditNameViewModel extends ViewModel {
|
||||
|
||||
private final ProfileEditNameRepository repo;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final MutableLiveData<Boolean> loading;
|
||||
|
||||
private ProfileEditNameViewModel() {
|
||||
this.repo = new ProfileEditNameRepository();
|
||||
this.events = new SingleLiveEvent<>();
|
||||
this.loading = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
void onSubmitPressed(@NonNull String profileName) {
|
||||
loading.setValue(true);
|
||||
|
||||
repo.setProfileName(profileName, result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
events.postValue(Event.SUCCESS);
|
||||
break;
|
||||
case NETWORK_FAILURE:
|
||||
events.postValue(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
|
||||
loading.postValue(false);
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> isLoading() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
enum Event {
|
||||
SUCCESS, NETWORK_FAILURE
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ProfileEditNameViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public final class AvatarUtil {
|
||||
}
|
||||
|
||||
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context).toString())).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.whispersystems.libsignal.util.Medium;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
@@ -442,12 +443,12 @@ public class TextSecurePreferences {
|
||||
setStringPreference(context, PROFILE_KEY_PREF, key);
|
||||
}
|
||||
|
||||
public static void setProfileName(Context context, String name) {
|
||||
setStringPreference(context, PROFILE_NAME_PREF, name);
|
||||
public static void setProfileName(Context context, ProfileName name) {
|
||||
setStringPreference(context, PROFILE_NAME_PREF, name.serialize());
|
||||
}
|
||||
|
||||
public static String getProfileName(Context context) {
|
||||
return getStringPreference(context, PROFILE_NAME_PREF, null);
|
||||
public static ProfileName getProfileName(Context context) {
|
||||
return ProfileName.fromSerialized(getStringPreference(context, PROFILE_NAME_PREF, null));
|
||||
}
|
||||
|
||||
public static void setProfileAvatarId(Context context, int id) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.util.cjkv;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class CJKVUtil {
|
||||
|
||||
private CJKVUtil() {
|
||||
}
|
||||
|
||||
public static boolean isCJKV(@Nullable String value) {
|
||||
if (value == null || value.length() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int offset = 0; offset < value.length(); ) {
|
||||
int codepoint = Character.codePointAt(value, offset);
|
||||
|
||||
if (!isCodepointCJKV(codepoint)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += Character.charCount(codepoint);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isCodepointCJKV(int codepoint) {
|
||||
if (codepoint == (int)' ') return true;
|
||||
|
||||
Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
|
||||
|
||||
return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT.equals(block) ||
|
||||
Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(block) ||
|
||||
Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS.equals(block) ||
|
||||
Character.UnicodeBlock.KANGXI_RADICALS.equals(block) ||
|
||||
Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS.equals(block) ||
|
||||
Character.UnicodeBlock.HIRAGANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA.equals(block) ||
|
||||
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO.equals(block) ||
|
||||
Character.UnicodeBlock.HANGUL_SYLLABLES.equals(block) ||
|
||||
Character.isIdeographic(codepoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.util.text;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
public final class AfterTextChanged implements TextWatcher {
|
||||
|
||||
private final Consumer<Editable> afterTextChangedConsumer;
|
||||
|
||||
public AfterTextChanged(@NonNull Consumer<Editable> afterTextChangedConsumer) {
|
||||
this.afterTextChangedConsumer = afterTextChangedConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
afterTextChangedConsumer.accept(s);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user