diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 780b595a0a..8e94898866 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -409,10 +409,10 @@
-
+
-
-
() {
- @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() {
- @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() {
- @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() {
- @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() {
- @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();
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
index 162aa7ce00..71f412d8dc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
@@ -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,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java
index d02c7beaa0..658d25c2e1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java
@@ -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> {
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;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
index 94e1519f68..7ca54443ca 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
index 3b5c955c85..cc61324798 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallScreen.java
@@ -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());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
index bdf961a75f..c7a1a83893 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
@@ -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 });
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java
index 04cb4c754d..efcc1ce6c8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java
@@ -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())
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java
index 860d287157..2c902ebabb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contactshare/ContactNameEditViewModel.java
@@ -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);
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
index fcc4d10df7..d1ce3e1250 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
index f406a7d65a..7bd5335adf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 42c941bfc4..7354840fdf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -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 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;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index e2d99ecf09..64cb462bb7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java
index 41d8920045..dee0b39684 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsRepository.java
@@ -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 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)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index 2c1bd5caf5..8b0697c8cc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -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());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java
new file mode 100644
index 0000000000..d5de641960
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java
@@ -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);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java
index 3de75c9ac6..3bd44b508a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java
@@ -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);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
index b34f09f58f..e8e4c96d2b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java
@@ -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)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
index 9a78bf66a8..9b6dc73614 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
@@ -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());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
index 0d76a42b66..83e8765235 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java
@@ -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()))))
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java
new file mode 100644
index 0000000000..e3248e7cb8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java
@@ -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 CREATOR = new Creator() {
+ @Override
+ public ProfileName createFromParcel(Parcel in) {
+ return new ProfileName(in);
+ }
+
+ @Override
+ public ProfileName[] newArray(int size) {
+ return new ProfileName[size];
+ }
+ };
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java
new file mode 100644
index 0000000000..25505a9ae7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java
@@ -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();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java
new file mode 100644
index 0000000000..3e7b5d7e37
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java
@@ -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() {
+ @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 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();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java
new file mode 100644
index 0000000000..0b42ea9da9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java
@@ -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 profileNameConsumer) {
+ ProfileName storedProfileName = TextSecurePreferences.getProfileName(context);
+ if (!storedProfileName.isEmpty()) {
+ profileNameConsumer.accept(storedProfileName);
+ } else if (!excludeSystem) {
+ SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener() {
+ @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 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() {
+ @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 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> callback) {
+ callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context)));
+ SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal()));
+ }
+
+ @WorkerThread
+ private @NonNull Optional 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
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java
new file mode 100644
index 0000000000..edbdefdb9c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java
@@ -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 givenName = new MutableLiveData<>();
+ private final MutableLiveData familyName = new MutableLiveData<>();
+ private final LiveData internalProfileName = Transformations.map(new LiveDataPair<>(givenName, familyName),
+ pair -> ProfileName.fromParts(pair.first(), pair.second()));
+ private final MutableLiveData internalAvatar = new MutableLiveData<>();
+ private final MutableLiveData> 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() {
+ return internalProfileName;
+ }
+
+ public LiveData avatar() {
+ return Transformations.distinctUntilChanged(internalAvatar);
+ }
+
+ public LiveData> 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 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 create(@NonNull Class modelClass) {
+ //noinspection unchecked
+ return (T) new EditProfileViewModel(repository);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
index 7fd06729e9..6a54cc66c2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -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;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java
index be36185a08..1e50edcbdb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java
@@ -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 participants;
- final String profileName;
+ final ProfileName profileName;
final Optional defaultSubscriptionId;
final RegisteredState registered;
final byte[] profileKey;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java
index 8f053c5933..63935e5915 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientExporter.java
@@ -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;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
index 634b1a00ea..7530933f43 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
@@ -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();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditActivityV2.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditActivityV2.java
deleted file mode 100644
index d1a19d378f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditActivityV2.java
+++ /dev/null
@@ -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());
- });
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewFragment.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewFragment.java
deleted file mode 100644
index 91d599d4d7..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewFragment.java
+++ /dev/null
@@ -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 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 profileName) {
- profileText.setText(profileName.or(""));
- }
-
- @SuppressLint("SetTextI18n")
- private void onUsernameChanged(@NonNull Optional username) {
- if (username.isPresent()) {
- usernameText.setText("@" + username.get());
- } else {
- usernameText.setText("");
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewRepository.java
deleted file mode 100644
index 49b4abe0d6..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewRepository.java
+++ /dev/null
@@ -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> callback) {
- executor.execute(() -> callback.onResult(getProfileAvatarInternal()));
- }
-
- void setProfileAvatar(@NonNull byte[] data, @NonNull Callback callback) {
- executor.execute(() -> callback.onResult(setProfileAvatarInternal(data)));
- }
-
- void deleteProfileAvatar(@NonNull Callback callback) {
- executor.execute(() -> callback.onResult(deleteProfileAvatarInternal()));
- }
-
- void getProfileName(@NonNull Callback> callback) {
- executor.execute(() -> callback.onResult(getProfileNameInternal()));
- }
-
- void getUsername(@NonNull Callback> callback) {
- executor.execute(() -> callback.onResult(getUsernameInternal()));
- }
-
- @WorkerThread
- private @NonNull Optional 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 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 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 {
- void onResult(@NonNull E result);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewViewModel.java
deleted file mode 100644
index 80d34511b6..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewViewModel.java
+++ /dev/null
@@ -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;
- private final MutableLiveData> avatar;
- private final MutableLiveData loading;
- private final MutableLiveData> profileName;
- private final MutableLiveData> 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> getAvatar() {
- return avatar;
- }
-
- @NonNull LiveData getLoading() {
- return loading;
- }
-
- @NonNull LiveData> getProfileName() {
- return profileName;
- }
-
- @NonNull LiveData> getUsername() {
- return username;
- }
-
- @NonNull LiveData 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 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 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 create(@NonNull Class modelClass) {
- //noinspection ConstantConditions
- return modelClass.cast(new ProfileEditOverviewViewModel());
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameFragment.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameFragment.java
deleted file mode 100644
index 77b87febd8..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameFragment.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameRepository.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameRepository.java
deleted file mode 100644
index 0b72541a5d..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameRepository.java
+++ /dev/null
@@ -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 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 {
- void onResult(@NonNull E result);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameViewModel.java
deleted file mode 100644
index 97861c3e1f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameViewModel.java
+++ /dev/null
@@ -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 events;
- private final MutableLiveData 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 getEvents() {
- return events;
- }
-
- @NonNull LiveData isLoading() {
- return loading;
- }
-
- enum Event {
- SUCCESS, NETWORK_FAILURE
- }
-
- static class Factory extends ViewModelProvider.NewInstanceFactory {
- @Override
- public @NonNull T create(@NonNull Class modelClass) {
- //noinspection ConstantConditions
- return modelClass.cast(new ProfileEditNameViewModel());
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java
index cb9a55482e..e726257ecd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java
@@ -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)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index 36f9507432..545c3ead3a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -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) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtil.java
new file mode 100644
index 0000000000..96baf94ff2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtil.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/text/AfterTextChanged.java b/app/src/main/java/org/thoughtcrime/securesms/util/text/AfterTextChanged.java
new file mode 100644
index 0000000000..39f361e184
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/text/AfterTextChanged.java
@@ -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 afterTextChangedConsumer;
+
+ public AfterTextChanged(@NonNull Consumer 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);
+ }
+}
diff --git a/app/src/main/res/drawable/circle_tintable_padded.xml b/app/src/main/res/drawable/circle_tintable_padded.xml
new file mode 100644
index 0000000000..e20e33bee4
--- /dev/null
+++ b/app/src/main/res/drawable/circle_tintable_padded.xml
@@ -0,0 +1,8 @@
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/layout/profile_create_activity.xml b/app/src/main/res/layout/profile_create_activity.xml
index de96ae5f02..ac5b7deded 100644
--- a/app/src/main/res/layout/profile_create_activity.xml
+++ b/app/src/main/res/layout/profile_create_activity.xml
@@ -1,183 +1,21 @@
-
-
+ android:layout_height="match_parent"
+ tools:context=".profiles.edit.EditProfileActivity">
-
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:navGraph="@navigation/edit_profile" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/app/src/main/res/layout/profile_create_fragment.xml b/app/src/main/res/layout/profile_create_fragment.xml
new file mode 100644
index 0000000000..07b4734831
--- /dev/null
+++ b/app/src/main/res/layout/profile_create_fragment.xml
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/profile_edit_activity_v2.xml b/app/src/main/res/layout/profile_edit_activity_v2.xml
deleted file mode 100644
index 208258c031..0000000000
--- a/app/src/main/res/layout/profile_edit_activity_v2.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/profile_edit_name_fragment.xml b/app/src/main/res/layout/profile_edit_name_fragment.xml
deleted file mode 100644
index f2b5549121..0000000000
--- a/app/src/main/res/layout/profile_edit_name_fragment.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/profile_edit_overview_fragment.xml b/app/src/main/res/layout/profile_edit_overview_fragment.xml
deleted file mode 100644
index 94a6354894..0000000000
--- a/app/src/main/res/layout/profile_edit_overview_fragment.xml
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/username_edit_fragment.xml b/app/src/main/res/layout/username_edit_fragment.xml
index 818fea90e4..09bd1c35eb 100644
--- a/app/src/main/res/layout/username_edit_fragment.xml
+++ b/app/src/main/res/layout/username_edit_fragment.xml
@@ -15,7 +15,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
- android:hint="@string/ProfileEditOverviewFragment_create_a_username"
+ android:hint="@string/CreateProfileActivity__create_a_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
diff --git a/app/src/main/res/navigation/edit_profile.xml b/app/src/main/res/navigation/edit_profile.xml
new file mode 100644
index 0000000000..3f21938693
--- /dev/null
+++ b/app/src/main/res/navigation/edit_profile.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/profile_edit.xml b/app/src/main/res/navigation/profile_edit.xml
deleted file mode 100644
index 5d5e884a8b..0000000000
--- a/app/src/main/res/navigation/profile_edit.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
index adb103844f..640068bf82 100644
--- a/app/src/main/res/values/integers.xml
+++ b/app/src/main/res/values/integers.xml
@@ -4,4 +4,6 @@
400
380
10
+
+ 26
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4044f89695..e8cc2598ff 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -312,12 +312,9 @@
Verified
- Your profile info
Error setting profile photo
Problem setting profile
Profile photo
- Too long
- Profile Name
Set up your profile
Signal profiles are end-to-end encrypted, and the Signal service never has access to this information.
Set avatar
@@ -646,19 +643,6 @@
The version of Google Play Services you have installed is not functioning correctly. Please reinstall Google Play Services and try again.
-
- Profile name
- Your profile name can be seen by your contacts and by other users or groups when you initiate a conversation or accept a conversation request.
- Save
-
-
- Profile
- Profile name
- Username
- Create a profile name
- Create a username
- Your Signal Profile can be seen by your contacts and by other users or groups when you initiate a conversation or accept a conversation request. Tap here to learn more.
-
Rate this app
If you enjoy using this app, please take a moment to help us by rating it.
@@ -1263,10 +1247,11 @@
To send media and group messages, tap \'OK\' and complete the requested settings. The MMS settings for your carrier can generally be located by searching for \'your carrier APN\'. You will only need to do this once.
- Set later
- FINISH
- Who can see this information?
- Your name
+ First name (required)
+ Last name (optional)
+ Next
+ Username
+ Create a username
Shared media
diff --git a/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java b/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java
new file mode 100644
index 0000000000..3e2e6c4b9a
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java
@@ -0,0 +1,164 @@
+package org.thoughtcrime.securesms.profiles;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public final class ProfileNameTest {
+
+@Test
+ public void givenEmpty_thenIExpectSaneDefaults() {
+ // GIVEN
+ ProfileName profileName = ProfileName.EMPTY;
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", profileName);
+ assertFalse("ProfileName should not be CJKV", profileName.isProfileNameCJKV());
+ assertEquals("ProfileName should have empty given name", "", profileName.getGivenName());
+ assertEquals("ProfileName should have empty family name", "", profileName.getFamilyName());
+ }
+
+ @Test
+ public void givenNullProfileName_whenIFromDataString_thenIExpectSaneDefaults() {
+ // GIVEN
+ ProfileName profileName = ProfileName.fromSerialized(null);
+
+ // THEN
+ assertSame(ProfileName.EMPTY, profileName);
+ }
+
+ @Test
+ public void givenProfileNameWithGivenNameOnly_whenIFromDataString_thenIExpectValidProfileName() {
+ // GIVEN
+ String profileName = "Given";
+
+ // WHEN
+ ProfileName name = ProfileName.fromSerialized(profileName);
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", name);
+ assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
+ assertEquals("ProfileName should have expected given name", profileName, name.getGivenName());
+ assertEquals("ProfileName should have empty family name", "", name.getFamilyName());
+ }
+
+ @Test
+ public void givenProfileNameWithEnglishGivenNameAndEnglishFamilyName_whenIFromDataString_thenIExpectValidProfileName() {
+ // GIVEN
+ String profileName = "Given\0Family";
+
+ // WHEN
+ ProfileName name = ProfileName.fromSerialized(profileName);
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", name);
+ assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
+ assertEquals("ProfileName should have expected given name", "Given", name.getGivenName());
+ assertEquals("ProfileName should have expected family name", "Family", name.getFamilyName());
+ }
+
+ @Test
+ public void givenProfileNameWithEnglishGivenNameAndCJKVFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
+ // GIVEN
+ String profileName = "Given\0码";
+
+ // WHEN
+ ProfileName name = ProfileName.fromSerialized(profileName);
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", name);
+ assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
+ assertEquals("ProfileName should have expected given name", "Given", name.getGivenName());
+ assertEquals("ProfileName should have expected family name", "码", name.getFamilyName());
+ }
+
+ @Test
+ public void givenProfileNameWithCJKVGivenNameAndCJKVFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
+ // GIVEN
+ String profileName = "统\0码";
+
+ // WHEN
+ ProfileName name = ProfileName.fromSerialized(profileName);
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", name);
+ assertTrue("ProfileName should be CJKV", name.isProfileNameCJKV());
+ assertEquals("ProfileName should have expected given name", "统", name.getGivenName());
+ assertEquals("ProfileName should have expected family name", "码", name.getFamilyName());
+ }
+
+ @Test
+ public void givenProfileNameWithCJKVGivenNameAndEnglishFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
+ // GIVEN
+ String profileName = "统\0Family";
+
+ // WHEN
+ ProfileName name = ProfileName.fromSerialized(profileName);
+
+ // THEN
+ assertNotNull("ProfileName should be non-null", name);
+ assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
+ assertEquals("ProfileName should have expected given name", "统", name.getGivenName());
+ assertEquals("ProfileName should have expected family name", "Family", name.getFamilyName());
+ }
+
+ @Test
+ public void givenProfileNameWithEmptyInputs_whenIToDataString_thenIExpectAnEmptyString() {
+ // GIVEN
+ ProfileName name = ProfileName.fromParts("", "");
+
+ // WHEN
+ String data = name.serialize();
+
+ // THEN
+ assertEquals("Blank String should be returned (For back compat)", "", data);
+ }
+
+ @Test
+ public void givenProfileNameWithEmptyGivenName_whenIToDataString_thenIExpectAnEmptyString() {
+ // GIVEN
+ ProfileName name = ProfileName.fromParts("", "Family");
+
+ // WHEN
+ String data = name.serialize();
+
+ // THEN
+ assertEquals("Blank String should be returned (For back compat)", "", data);
+ }
+
+ @Test
+ public void givenProfileNameWithGivenName_whenIToDataString_thenIExpectValidProfileName() {
+ // GIVEN
+ ProfileName name = ProfileName.fromParts("Given", "");
+
+ // WHEN
+ String data = name.serialize();
+
+ // THEN
+ assertEquals(data, "Given\0");
+ }
+
+ @Test
+ public void givenProfileNameWithGivenNameAndFamilyName_whenIToDataString_thenIExpectValidProfileName() {
+ // GIVEN
+ ProfileName name = ProfileName.fromParts("Given", "Family");
+
+ // WHEN
+ String data = name.serialize();
+
+ // THEN
+ assertEquals(data, "Given\0Family");
+ }
+
+ @Test
+ public void fromParts_with_long_name_parts() {
+ ProfileName name = ProfileName.fromParts("GivenSomeVeryLongNameSomeVeryLongName", "FamilySomeVeryLongNameSomeVeryLongName");
+
+ assertEquals("GivenSomeVeryLongNameSomeV", name.getGivenName());
+ assertEquals("FamilySomeVeryLongNameSome", name.getFamilyName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
index e765414444..d86c5bc343 100644
--- a/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
+++ b/app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
@@ -8,6 +8,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
+import org.thoughtcrime.securesms.profiles.ProfileName;
import org.whispersystems.libsignal.util.guava.Optional;
import static android.provider.ContactsContract.Intents.Insert.NAME;
@@ -24,7 +25,7 @@ public final class RecipientExporterTest {
@Test
public void asAddContactIntent_with_phone_number() {
- Recipient recipient = givenPhoneRecipient("Alice", "+1555123456");
+ Recipient recipient = givenPhoneRecipient(ProfileName.fromParts("Alice", null), "+1555123456");
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
@@ -37,7 +38,7 @@ public final class RecipientExporterTest {
@Test
public void asAddContactIntent_with_email() {
- Recipient recipient = givenEmailRecipient("Bob", "bob@signal.org");
+ Recipient recipient = givenEmailRecipient(ProfileName.fromParts("Bob", null), "bob@signal.org");
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
@@ -48,7 +49,7 @@ public final class RecipientExporterTest {
assertNull(intent.getStringExtra(PHONE));
}
- private Recipient givenPhoneRecipient(String profileName, String phone) {
+ private Recipient givenPhoneRecipient(ProfileName profileName, String phone) {
Recipient recipient = mock(Recipient.class);
when(recipient.getProfileName()).thenReturn(profileName);
@@ -59,7 +60,7 @@ public final class RecipientExporterTest {
return recipient;
}
- private Recipient givenEmailRecipient(String profileName, String email) {
+ private Recipient givenEmailRecipient(ProfileName profileName, String email) {
Recipient recipient = mock(Recipient.class);
when(recipient.getProfileName()).thenReturn(profileName);
diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtilTest.java
new file mode 100644
index 0000000000..0841e710e8
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtilTest.java
@@ -0,0 +1,65 @@
+package org.thoughtcrime.securesms.util.cjkv;
+
+import android.app.Application;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import static org.junit.Assert.*;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = Config.NONE, application = Application.class)
+public class CJKVUtilTest {
+
+ private static final String CJKV_CHARS = "统码";
+ private static final String NON_CJKV_CHAR = "a";
+ private static final String MIXED_CHARS = CJKV_CHARS + NON_CJKV_CHAR;
+
+ @Test
+ public void givenAllCJKVChars_whenIsCJKV_thenIExpectTrue() {
+ // WHEN
+ boolean result = CJKVUtil.isCJKV(CJKV_CHARS);
+
+ //THEN
+ assertTrue(result);
+ }
+
+ @Test
+ public void givenNoCJKVChars_whenIsCJKV_thenIExpectFalse() {
+ // WHEN
+ boolean result = CJKVUtil.isCJKV(NON_CJKV_CHAR);
+
+ // THEN
+ assertFalse(result);
+ }
+
+ @Test
+ public void givenOneNonCJKVChar_whenIsCJKV_thenIExpectFalse() {
+ // WHEN
+ boolean result = CJKVUtil.isCJKV(MIXED_CHARS);
+
+ // THEN
+ assertFalse(result);
+ }
+
+ @Test
+ public void givenAnEmptyString_whenIsCJKV_thenIExpectTrue() {
+ // WHEN
+ boolean result = CJKVUtil.isCJKV("");
+
+ // THEN
+ assertTrue(result);
+ }
+
+ @Test
+ public void givenNull_whenIsCJKV_thenIExpectTrue() {
+ // WHEN
+ boolean result = CJKVUtil.isCJKV(null);
+
+ // THEN
+ assertTrue(result);
+ }
+
+}
\ No newline at end of file
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
index dc3365f289..db3c9c6b5f 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java
@@ -19,7 +19,7 @@ import javax.crypto.spec.SecretKeySpec;
public class ProfileCipher {
- public static final int NAME_PADDED_LENGTH = 26;
+ public static final int NAME_PADDED_LENGTH = 53;
private final byte[] key;
diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
index ab0b0ec509..b77b64d167 100644
--- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
+++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java
@@ -19,9 +19,9 @@ public class ProfileCipherTest extends TestCase {
public void testEncryptDecrypt() throws InvalidCiphertextException {
byte[] key = Util.getSecretBytes(32);
ProfileCipher cipher = new ProfileCipher(key);
- byte[] name = cipher.encryptName("Clement Duval".getBytes(), 26);
+ byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
byte[] plaintext = cipher.decryptName(name);
- assertEquals(new String(plaintext), "Clement Duval");
+ assertEquals(new String(plaintext), "Clement\0Duval");
}
public void testEmpty() throws Exception {