From 3907ec8b514acc5a1a4a77f75b7247a6e50d6adc Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 20 Dec 2019 16:12:22 -0400 Subject: [PATCH] Add support for setting an optional last name in profiles. --- app/src/main/AndroidManifest.xml | 13 +- .../ApplicationPreferencesActivity.java | 14 +- .../securesms/CreateProfileActivity.java | 447 ------------------ .../securesms/ExperienceUpgradeActivity.java | 6 +- .../securesms/GroupMembersDialog.java | 6 +- .../securesms/components/FromTextView.java | 6 +- .../components/webrtc/WebRtcCallScreen.java | 6 +- .../securesms/contacts/ContactRepository.java | 4 +- .../contacts/sync/StorageSyncHelper.java | 7 +- .../ContactNameEditViewModel.java | 54 +-- .../conversation/ConversationItem.java | 4 +- .../conversation/ConversationTitleView.java | 4 +- .../securesms/database/RecipientDatabase.java | 48 +- .../database/helpers/SQLCipherOpenHelper.java | 18 +- .../insights/InsightsRepository.java | 3 +- .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/ProfileUploadJob.java | 108 +++++ .../securesms/jobs/RefreshOwnProfileJob.java | 10 +- .../securesms/jobs/RetrieveProfileJob.java | 17 +- .../securesms/jobs/RotateProfileKeyJob.java | 3 +- .../widgets/ProfilePreference.java | 3 +- .../securesms/profiles/ProfileName.java | 155 ++++++ .../profiles/edit/EditProfileActivity.java | 47 ++ .../profiles/edit/EditProfileFragment.java | 351 ++++++++++++++ .../profiles/edit/EditProfileRepository.java | 165 +++++++ .../profiles/edit/EditProfileViewModel.java | 92 ++++ .../securesms/recipients/Recipient.java | 11 +- .../recipients/RecipientDetails.java | 3 +- .../recipients/RecipientExporter.java | 3 +- .../RegistrationCompleteFragment.java | 5 +- .../usernames/ProfileEditActivityV2.java | 64 --- .../ProfileEditOverviewFragment.java | 130 ----- .../ProfileEditOverviewRepository.java | 171 ------- .../ProfileEditOverviewViewModel.java | 212 --------- .../profile/ProfileEditNameFragment.java | 79 ---- .../profile/ProfileEditNameRepository.java | 57 --- .../profile/ProfileEditNameViewModel.java | 59 --- .../securesms/util/AvatarUtil.java | 2 +- .../securesms/util/TextSecurePreferences.java | 9 +- .../securesms/util/cjkv/CJKVUtil.java | 53 +++ .../securesms/util/text/AfterTextChanged.java | 29 ++ .../res/drawable/circle_tintable_padded.xml | 8 + .../res/layout/profile_create_activity.xml | 188 +------- .../res/layout/profile_create_fragment.xml | 237 ++++++++++ .../res/layout/profile_edit_activity_v2.xml | 24 - .../res/layout/profile_edit_name_fragment.xml | 49 -- .../layout/profile_edit_overview_fragment.xml | 159 ------- .../res/layout/username_edit_fragment.xml | 2 +- app/src/main/res/navigation/edit_profile.xml | 30 ++ app/src/main/res/navigation/profile_edit.xml | 41 -- app/src/main/res/values/integers.xml | 2 + app/src/main/res/values/strings.xml | 25 +- .../securesms/profiles/ProfileNameTest.java | 164 +++++++ .../recipients/RecipientExporterTest.java | 9 +- .../securesms/util/cjkv/CJKVUtilTest.java | 65 +++ .../api/crypto/ProfileCipher.java | 2 +- .../api/crypto/ProfileCipherTest.java | 4 +- 57 files changed, 1641 insertions(+), 1847 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/CreateProfileActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditActivityV2.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewFragment.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewRepository.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/ProfileEditOverviewViewModel.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameFragment.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameRepository.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/usernames/profile/ProfileEditNameViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/text/AfterTextChanged.java create mode 100644 app/src/main/res/drawable/circle_tintable_padded.xml create mode 100644 app/src/main/res/layout/profile_create_fragment.xml delete mode 100644 app/src/main/res/layout/profile_edit_activity_v2.xml delete mode 100644 app/src/main/res/layout/profile_edit_name_fragment.xml delete mode 100644 app/src/main/res/layout/profile_edit_overview_fragment.xml create mode 100644 app/src/main/res/navigation/edit_profile.xml delete mode 100644 app/src/main/res/navigation/profile_edit.xml create mode 100644 app/src/test/java/org/thoughtcrime/securesms/profiles/ProfileNameTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/cjkv/CJKVUtilTest.java 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" /> - - - - - - - - - - - - - - - - - - - - - - - - - - -