Add support for setting an optional last name in profiles.

master
Alex Hart 2019-12-20 16:12:22 -04:00 committed by Greyson Parrelli
parent f2b9bf0b8c
commit 3907ec8b51
57 changed files with 1641 additions and 1847 deletions

View File

@ -409,10 +409,10 @@
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/TextSecure.DarkTheme"/>
<activity android:name=".CreateProfileActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".profiles.edit.EditProfileActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearProfileAvatarActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
@ -449,11 +449,6 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity
android:name=".usernames.ProfileEditActivityV2"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".MainActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:launchMode="singleTask"

View File

@ -38,8 +38,8 @@ import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.usernames.ProfileEditActivityV2;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
@ -266,14 +266,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (FeatureFlags.USERNAMES) {
requireActivity().startActivity(ProfileEditActivityV2.getLaunchIntent(requireContext()));
} else {
Intent intent = new Intent(preference.getContext(), CreateProfileActivity.class);
intent.putExtra(CreateProfileActivity.EXCLUDE_SYSTEM, true);
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
requireActivity().startActivity(intent);
}
requireActivity().startActivity(intent);
return true;
}
}

View File

@ -1,447 +0,0 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
@SuppressLint("StaticFieldLeak")
public class CreateProfileActivity extends BaseActionBarActivity {
private static final String TAG = CreateProfileActivity.class.getSimpleName();
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private InputAwareLayout container;
private ImageView avatar;
private CircularProgressButton finishButton;
private LabeledEditText name;
private EmojiToggle emojiToggle;
private MediaKeyboard mediaKeyboard;
private View reveal;
private Intent nextIntent;
private byte[] avatarBytes;
private File captureFile;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
setContentView(R.layout.profile_create_activity);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
initializeResources();
initializeEmojiInput();
initializeProfileName(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
initializeProfileAvatar(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
public void onBackPressed() {
if (container.isInputOpen()) container.hideCurrentInput(name.getInput());
else super.onBackPressed();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (container.getCurrentInput() == mediaKeyboard) {
container.hideAttachedInput(true);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case AvatarSelection.REQUEST_CODE_AVATAR:
if (resultCode == Activity.RESULT_OK) {
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
Uri inputFile = (data != null ? data.getData() : null);
if (inputFile == null && captureFile != null) {
inputFile = Uri.fromFile(captureFile);
}
if (data != null && data.getBooleanExtra("delete", false)) {
avatarBytes = null;
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(this, getResources().getColor(R.color.grey_400)));
} else {
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
}
}
break;
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
if (resultCode == Activity.RESULT_OK) {
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
try {
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(CreateProfileActivity.this, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
return result.getBitmap();
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(avatarBytes)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(avatar);
} else {
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
}
}
}.execute();
}
break;
}
}
private void initializeResources() {
TextView skipButton = ViewUtil.findById(this, R.id.skip_button);
this.avatar = ViewUtil.findById(this, R.id.avatar);
this.name = ViewUtil.findById(this, R.id.name);
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
this.mediaKeyboard = ViewUtil.findById(this, R.id.emoji_drawer);
this.container = ViewUtil.findById(this, R.id.container);
this.finishButton = ViewUtil.findById(this, R.id.finish_button);
this.reveal = ViewUtil.findById(this, R.id.reveal);
this.nextIntent = getIntent().getParcelableExtra(NEXT_INTENT);
this.avatar.setOnClickListener(view -> Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.onAnyResult(this::startAvatarSelection)
.execute());
this.name.getInput().addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) {
name.getInput().setError(getString(R.string.CreateProfileActivity_too_long));
finishButton.setEnabled(false);
} else if (name.getInput().getError() != null || !finishButton.isEnabled()) {
name.getInput().setError(null);
finishButton.setEnabled(true);
}
}
});
this.finishButton.setOnClickListener(view -> {
this.finishButton.setIndeterminateProgressMode(true);
this.finishButton.setProgress(50);
handleUpload();
});
skipButton.setOnClickListener(view -> {
if (nextIntent != null) startActivity(nextIntent);
finish();
});
}
private void initializeProfileName(boolean excludeSystem) {
if (!TextUtils.isEmpty(TextSecurePreferences.getProfileName(this))) {
String profileName = TextSecurePreferences.getProfileName(this);
name.setText(profileName);
name.getInput().setSelection(profileName.length(), profileName.length());
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileName(this).addListener(new ListenableFuture.Listener<String>() {
@Override
public void onSuccess(String result) {
if (!TextUtils.isEmpty(result)) {
name.setText(result);
name.getInput().setSelection(result.length(), result.length());
}
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
}
});
}
}
private void initializeProfileAvatar(boolean excludeSystem) {
RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.getAvatarFile(this, selfId).exists() && AvatarHelper.getAvatarFile(this, selfId).length() > 0) {
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
try {
return Util.readFully(AvatarHelper.getInputStreamFor(CreateProfileActivity.this, selfId));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
.circleCrop()
.into(avatar);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileAvatar(this, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
@Override
public void onSuccess(byte[] result) {
if (result != null) {
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
.circleCrop()
.into(avatar);
}
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
}
});
}
}
private void initializeEmojiInput() {
this.emojiToggle.attach(mediaKeyboard);
this.emojiToggle.setOnClickListener(v -> {
if (container.getCurrentInput() == mediaKeyboard) {
container.showSoftkey(name.getInput());
} else {
container.show(name.getInput(), mediaKeyboard);
}
});
this.mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
@Override
public void onKeyEvent(KeyEvent keyEvent) {
name.dispatchKeyEvent(keyEvent);
}
@Override
public void onEmojiSelected(String emoji) {
final int start = name.getInput().getSelectionStart();
final int end = name.getInput().getSelectionEnd();
name.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
name.getInput().setSelection(start + emoji.length());
}
}));
this.container.addOnKeyboardShownListener(() -> emojiToggle.setToMedia());
this.name.setOnClickListener(v -> container.showSoftkey(name.getInput()));
}
private void startAvatarSelection() {
captureFile = AvatarSelection.startAvatarSelection(this, avatarBytes != null, true);
}
private void handleUpload() {
final String name;
final StreamDetails avatar;
if (TextUtils.isEmpty(this.name.getText().toString())) name = null;
else name = this.name.getText().toString();
if (avatarBytes == null || avatarBytes.length == 0) avatar = null;
else avatar = new StreamDetails(new ByteArrayInputStream(avatarBytes),
"image/jpeg", avatarBytes.length);
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
Context context = CreateProfileActivity.this;
byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
try {
accountManager.setProfileName(profileKey, name);
TextSecurePreferences.setProfileName(context, name);
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), name);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
try {
accountManager.setProfileAvatar(profileKey, avatar);
AvatarHelper.setAvatar(CreateProfileActivity.this, Recipient.self().getId(), avatarBytes);
TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt());
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
return true;
}
@Override
public void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
if (captureFile != null) captureFile.delete();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
else handleFinishedLegacy();
} else {
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void handleFinishedLegacy() {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
finish();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void handleFinishedLollipop() {
int[] finishButtonLocation = new int[2];
int[] revealLocation = new int[2];
finishButton.getLocationInWindow(finishButtonLocation);
reveal.getLocationInWindow(revealLocation);
int finishX = finishButtonLocation[0] - revealLocation[0];
int finishY = finishButtonLocation[1] - revealLocation[1];
finishX += finishButton.getWidth() / 2;
finishY += finishButton.getHeight() / 2;
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
animation.setDuration(500);
animation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
finish();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
reveal.setVisibility(View.VISIBLE);
animation.start();
}
}

View File

@ -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,

View File

@ -5,13 +5,11 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import androidx.appcompat.app.AlertDialog;
import android.text.TextUtils;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import java.util.LinkedList;
import java.util.List;
@ -119,8 +117,8 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
String name = recipient.toShortString(context);
if (recipient.getName(context) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
name += " ~" + recipient.getProfileName();
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
name += " ~" + recipient.getProfileName().toString();
}
return name;

View File

@ -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);

View File

@ -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());
}

View File

@ -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 });

View File

@ -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())

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();

View File

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
@ -79,7 +80,6 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String SYSTEM_INFO_PENDING = "system_info_pending";
private static final String PROFILE_KEY = "profile_key";
public static final String SIGNAL_PROFILE_NAME = "signal_profile_name";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
@ -87,7 +87,11 @@ public class RecipientDatabase extends Database {
private static final String UUID_SUPPORTED = "uuid_supported";
private static final String STORAGE_SERVICE_KEY = "storage_service_key";
private static final String DIRTY = "dirty";
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
private static final String PROFILE_JOINED_NAME = "profile_joined_name";
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
private static final String SORT_NAME = "sort_name";
private static final String IDENTITY_STATUS = "identity_status";
private static final String IDENTITY_KEY = "identity_key";
@ -97,7 +101,7 @@ public class RecipientDatabase extends Database {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, UUID_SUPPORTED, STORAGE_SERVICE_KEY, DIRTY
};
@ -116,7 +120,8 @@ public class RecipientDatabase extends Database {
};
private static final String[] ID_PROJECTION = new String[]{ID};
public static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, SIGNAL_PROFILE_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + USERNAME + ") AS " + SORT_NAME};
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + SYSTEM_DISPLAY_NAME + ", " + PROFILE_JOINED_NAME + ", " + PROFILE_GIVEN_NAME + ", " + USERNAME + ") AS " + SORT_NAME};
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
.map(columnName -> TABLE_NAME + "." + columnName)
.toList();
@ -237,7 +242,9 @@ public class RecipientDatabase extends Database {
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " +
PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " +
PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " +
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " +
PROFILE_SHARING + " INTEGER DEFAULT 0, " +
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
@ -471,8 +478,12 @@ public class RecipientDatabase extends Database {
values.put(UUID, contact.getAddress().getUuid().get().toString());
}
ProfileName profileName = ProfileName.fromSerialized(contact.getProfileName().orNull());
values.put(PHONE, contact.getAddress().getNumber().orNull());
values.put(SIGNAL_PROFILE_NAME, contact.getProfileName().orNull());
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
values.put(PROFILE_JOINED_NAME, profileName.toString());
values.put(PROFILE_KEY, contact.getProfileKey().orNull());
// TODO [greyson] Username
values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0");
@ -551,7 +562,8 @@ public class RecipientDatabase extends Database {
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
@ -605,7 +617,7 @@ public class RecipientDatabase extends Database {
RegisteredState.fromId(registeredState),
profileKey, systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
signalProfileName, signalProfileAvatar, profileSharing,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar, profileSharing,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
forceSmsSelection, uuidSupported, InsightsBannerTier.fromId(insightsBannerTier),
storageKey, identityKey, identityStatus);
@ -751,9 +763,11 @@ public class RecipientDatabase extends Database {
}
}
public void setProfileName(@NonNull RecipientId id, @Nullable String profileName) {
public void setProfileName(@NonNull RecipientId id, @NonNull ProfileName profileName) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(SIGNAL_PROFILE_NAME, profileName);
contentValues.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
contentValues.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
contentValues.put(PROFILE_JOINED_NAME, profileName.toString());
if (update(id, contentValues)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
@ -1000,9 +1014,9 @@ public class RecipientDatabase extends Database {
REGISTERED + " = ? AND " +
GROUP_ID + " IS NULL AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + PROFILE_SHARING + " = ?) AND " +
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SIGNAL_PROFILE_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
"(" + SYSTEM_DISPLAY_NAME + " NOT NULL OR " + SEARCH_PROFILE_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1" };
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
@ -1018,11 +1032,11 @@ public class RecipientDatabase extends Database {
"(" +
PHONE + " LIKE ? OR " +
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
SIGNAL_PROFILE_NAME + " LIKE ? OR " +
SEARCH_PROFILE_NAME + " LIKE ? OR " +
USERNAME + " LIKE ?" +
")";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), "1", query, query, query, query };
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SIGNAL_PROFILE_NAME + ", " + PHONE;
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
@ -1066,7 +1080,7 @@ public class RecipientDatabase extends Database {
String selection = BLOCKED + " = ? AND " +
"(" +
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
SIGNAL_PROFILE_NAME + " LIKE ? OR " +
SEARCH_PROFILE_NAME + " LIKE ? OR " +
PHONE + " LIKE ? OR " +
EMAIL + " LIKE ?" +
")";
@ -1342,7 +1356,7 @@ public class RecipientDatabase extends Database {
private final String systemContactPhoto;
private final String systemPhoneLabel;
private final String systemContactUri;
private final String signalProfileName;
private final ProfileName signalProfileName;
private final String signalProfileAvatar;
private final boolean profileSharing;
private final String notificationChannel;
@ -1374,7 +1388,7 @@ public class RecipientDatabase extends Database {
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@Nullable String systemContactUri,
@Nullable String signalProfileName,
@NonNull ProfileName signalProfileName,
@Nullable String signalProfileAvatar,
boolean profileSharing,
@Nullable String notificationChannel,
@ -1508,7 +1522,7 @@ public class RecipientDatabase extends Database {
return systemContactUri;
}
public @Nullable String getProfileName() {
public @NonNull ProfileName getProfileName() {
return signalProfileName;
}

View File

@ -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();

View File

@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -68,7 +67,7 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository
public void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> avatarConsumer) {
SimpleTask.run(() -> {
Recipient self = Recipient.self().resolve();
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context).toString())).or("");
MaterialColor fallbackColor = self.getColor();
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {

View File

@ -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());

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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)) {

View File

@ -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());

View File

@ -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()))))

View File

@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.profiles;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.util.cjkv.CJKVUtil;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import java.nio.charset.StandardCharsets;
public final class ProfileName implements Parcelable {
public static final ProfileName EMPTY = new ProfileName("", "");
private static final int MAX_PART_LENGTH = (ProfileCipher.NAME_PADDED_LENGTH - 1) / 2;
private final String givenName;
private final String familyName;
private final String joinedName;
private ProfileName(@Nullable String givenName, @Nullable String familyName) {
this.givenName = sanitize(givenName);
this.familyName = sanitize(familyName);
this.joinedName = getJoinedName(this.givenName, this.familyName);
}
private ProfileName(Parcel in) {
this(in.readString(), in.readString());
}
public @NonNull
String getGivenName() {
return givenName;
}
public @NonNull
String getFamilyName() {
return familyName;
}
public boolean isProfileNameCJKV() {
return isCJKV(givenName, familyName);
}
public boolean isEmpty() {
return joinedName.isEmpty();
}
public @NonNull String serialize() {
if (isEmpty()) {
return "";
}
return String.format("%s\0%s", givenName, familyName);
}
@Override
public @NonNull String toString() {
return joinedName;
}
/**
* Deserializes a profile name, trims if exceeds the limits.
*/
public static @NonNull ProfileName fromSerialized(@Nullable String profileName) {
if (profileName == null) {
return EMPTY;
}
String[] parts = profileName.split("\0");
if (parts.length == 0) {
return EMPTY;
} else if (parts.length == 1) {
return fromParts(parts[0], "");
} else {
return fromParts(parts[0], parts[1]);
}
}
/**
* Creates a profile name, trimming chars until it fits the limits.
*/
public static @NonNull ProfileName fromParts(@Nullable String givenName, @Nullable String familyName) {
if (givenName == null || givenName.isEmpty()) return EMPTY;
return new ProfileName(givenName, familyName);
}
private static @NonNull String sanitize(@Nullable String name) {
if (name == null) return "";
// At least one byte per char, so shorten string to reduce loop
if (name.length() > ProfileName.MAX_PART_LENGTH) {
name = name.substring(0, ProfileName.MAX_PART_LENGTH);
}
// Remove one char at a time until fits in byte allowance
while (name.getBytes(StandardCharsets.UTF_8).length > ProfileName.MAX_PART_LENGTH) {
name = name.substring(0, name.length() - 1);
}
return name;
}
private static @NonNull String getJoinedName(@NonNull String givenName, @NonNull String familyName) {
if (givenName.isEmpty() && familyName.isEmpty()) return "";
else if (givenName.isEmpty()) return familyName;
else if (familyName.isEmpty()) return givenName;
else if (isCJKV(givenName, familyName)) return String.format("%s %s",
familyName,
givenName);
else return String.format("%s %s",
givenName,
familyName);
}
private static boolean isCJKV(@NonNull String givenName, @NonNull String familyName) {
if (givenName.isEmpty() && familyName.isEmpty()) {
return false;
} else {
return Stream.of(givenName, familyName)
.filterNot(String::isEmpty)
.reduce(true, (a, s) -> a && CJKVUtil.isCJKV(s));
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(givenName);
dest.writeString(familyName);
}
public static final Creator<ProfileName> CREATOR = new Creator<ProfileName>() {
@Override
public ProfileName createFromParcel(Parcel in) {
return new ProfileName(in);
}
@Override
public ProfileName[] newArray(int size) {
return new ProfileName[size];
}
};
}

View File

@ -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();
}
}

View File

@ -0,0 +1,351 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Selection;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.NavDirections;
import androidx.navigation.Navigation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPLAY_USERNAME;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_INTENT;
public class EditProfileFragment extends Fragment {
private static final String TAG = Log.tag(EditProfileFragment.class);
private ImageView avatar;
private CircularProgressButton finishButton;
private EditText givenName;
private EditText familyName;
private View reveal;
private TextView preview;
private View usernameLabel;
private View usernameEditButton;
private TextView username;
private Intent nextIntent;
private File captureFile;
private EditProfileViewModel viewModel;
private Controller controller;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Controller) {
controller = (Controller) context;
} else {
throw new IllegalStateException("Context must subclass Controller");
}
}
public static EditProfileFragment create(boolean excludeSystem,
Intent nextIntent,
boolean displayUsernameField,
@StringRes int nextButtonText) {
EditProfileFragment fragment = new EditProfileFragment();
Bundle args = new Bundle();
args.putBoolean(EXCLUDE_SYSTEM, excludeSystem);
args.putParcelable(NEXT_INTENT, nextIntent);
args.putBoolean(DISPLAY_USERNAME, displayUsernameField);
args.putInt(NEXT_BUTTON_TEXT, nextButtonText);
fragment.setArguments(args);
return fragment;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.profile_create_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeResources(view);
initializeViewModel(getArguments().getBoolean(EXCLUDE_SYSTEM, false));
initializeProfileName();
initializeProfileAvatar();
initializeUsername();
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case AvatarSelection.REQUEST_CODE_AVATAR:
if (resultCode == Activity.RESULT_OK) {
Uri outputFile = Uri.fromFile(new File(requireActivity().getCacheDir(), "cropped"));
Uri inputFile = (data != null ? data.getData() : null);
if (inputFile == null && captureFile != null) {
inputFile = Uri.fromFile(captureFile);
}
if (data != null && data.getBooleanExtra("delete", false)) {
viewModel.setAvatar(null);
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
} else {
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
}
}
break;
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
if (resultCode == Activity.RESULT_OK) {
new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(Void... params) {
try {
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(requireActivity(), AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
return result.getBitmap();
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
viewModel.setAvatar(result);
GlideApp.with(EditProfileFragment.this)
.load(result)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(avatar);
} else {
Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
}
}
}.execute();
}
break;
}
}
private void initializeViewModel(boolean excludeSystem) {
EditProfileRepository repository = new EditProfileRepository(requireContext(), excludeSystem);
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository);
viewModel = ViewModelProviders.of(this, factory).get(EditProfileViewModel.class);
}
private void initializeResources(@NonNull View view) {
this.avatar = view.findViewById(R.id.avatar);
this.givenName = view.findViewById(R.id.given_name);
this.familyName = view.findViewById(R.id.family_name);
this.finishButton = view.findViewById(R.id.finish_button);
this.reveal = view.findViewById(R.id.reveal);
this.preview = view.findViewById(R.id.name_preview);
this.username = view.findViewById(R.id.profile_overview_username);
this.usernameEditButton = view.findViewById(R.id.profile_overview_username_edit_button);
this.usernameLabel = view.findViewById(R.id.profile_overview_username_label);
this.nextIntent = getArguments().getParcelable(NEXT_INTENT);
if (FeatureFlags.USERNAMES && getArguments().getBoolean(DISPLAY_USERNAME, false)) {
username.setVisibility(View.VISIBLE);
usernameEditButton.setVisibility(View.VISIBLE);
usernameLabel.setVisibility(View.VISIBLE);
}
this.avatar.setOnClickListener(v -> Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.onAnyResult(this::startAvatarSelection)
.execute());
this.givenName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setGivenName(s.toString())));
this.familyName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setFamilyName(s.toString())));
this.finishButton.setOnClickListener(v -> {
this.finishButton.setIndeterminateProgressMode(true);
this.finishButton.setProgress(50);
handleUpload();
});
this.finishButton.setText(getArguments().getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
this.usernameEditButton.setOnClickListener(v -> {
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
Navigation.findNavController(v).navigate(action);
});
}
private void initializeProfileName() {
viewModel.profileName().observe(this, profileName -> {
updateFieldIfNeeded(givenName, profileName.getGivenName());
updateFieldIfNeeded(familyName, profileName.getFamilyName());
finishButton.setEnabled(!profileName.isEmpty());
finishButton.setAlpha(!profileName.isEmpty() ? 1f : 0.5f);
preview.setText(profileName.toString());
});
}
private void initializeProfileAvatar() {
viewModel.avatar().observe(this, bytes -> {
if (bytes == null) return;
GlideApp.with(this)
.load(bytes)
.circleCrop()
.into(avatar);
});
}
private void initializeUsername() {
viewModel.username().observe(this, this::onUsernameChanged);
}
private void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) {
if (!field.getText().toString().equals(value)) {
boolean setSelectionToEnd = field.getText().length() == 0;
field.setText(value);
if (setSelectionToEnd) {
field.setSelection(field.getText().length());
}
}
}
@SuppressLint("SetTextI18n")
private void onUsernameChanged(@NonNull Optional<String> username) {
if (username.isPresent()) {
this.username.setText("@" + username.get());
} else {
this.username.setText("");
}
}
private void startAvatarSelection() {
captureFile = AvatarSelection.startAvatarSelection(this, viewModel.hasAvatar(), true);
}
private void handleUpload() {
viewModel.submitProfile(uploadResult -> {
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
if (captureFile != null) captureFile.delete();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
else handleFinishedLegacy();
} else {
Toast.makeText(requireContext(), R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
}
});
}
private void handleFinishedLegacy() {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
controller.onProfileNameUploadCompleted();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void handleFinishedLollipop() {
int[] finishButtonLocation = new int[2];
int[] revealLocation = new int[2];
finishButton.getLocationInWindow(finishButtonLocation);
reveal.getLocationInWindow(revealLocation);
int finishX = finishButtonLocation[0] - revealLocation[0];
int finishY = finishButtonLocation[1] - revealLocation[1];
finishX += finishButton.getWidth() / 2;
finishY += finishButton.getHeight() / 2;
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
animation.setDuration(500);
animation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
finishButton.setProgress(0);
if (nextIntent != null) startActivity(nextIntent);
controller.onProfileNameUploadCompleted();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
reveal.setVisibility(View.VISIBLE);
animation.start();
}
public interface Controller {
void onProfileNameUploadCompleted();
}
}

View File

@ -0,0 +1,165 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
class EditProfileRepository {
private static final String TAG = Log.tag(EditProfileRepository.class);
private final Context context;
private final boolean excludeSystem;
EditProfileRepository(@NonNull Context context, boolean excludeSystem) {
this.context = context.getApplicationContext();
this.excludeSystem = excludeSystem;
}
void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer) {
ProfileName storedProfileName = TextSecurePreferences.getProfileName(context);
if (!storedProfileName.isEmpty()) {
profileNameConsumer.accept(storedProfileName);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener<String>() {
@Override
public void onSuccess(String result) {
if (!TextUtils.isEmpty(result)) {
profileNameConsumer.accept(ProfileName.fromSerialized(result));
} else {
profileNameConsumer.accept(storedProfileName);
}
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
profileNameConsumer.accept(storedProfileName);
}
});
} else {
profileNameConsumer.accept(storedProfileName);
}
}
void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
SimpleTask.run(() -> {
try {
return Util.readFully(AvatarHelper.getInputStreamFor(context, selfId));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}, avatarConsumer::accept);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileAvatar(context, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
@Override
public void onSuccess(byte[] result) {
avatarConsumer.accept(result);
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
avatarConsumer.accept(null);
}
});
}
}
void uploadProfile(@NonNull ProfileName profileName, @Nullable byte[] avatar, @NonNull Consumer<UploadResult> uploadResultConsumer) {
SimpleTask.run(() -> {
TextSecurePreferences.setProfileName(context, profileName);
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
try {
AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar);
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
} catch (IOException e) {
return UploadResult.ERROR_FILE_IO;
}
ApplicationDependencies.getJobManager()
.startChain(new ProfileUploadJob())
.then(Arrays.asList(new MultiDeviceProfileKeyUpdateJob(), new MultiDeviceProfileContentUpdateJob()))
.enqueue();
return UploadResult.SUCCESS;
}, uploadResultConsumer::accept);
}
void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context)));
SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal()));
}
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = retrieveOwnProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
}
private SignalServiceProfile retrieveOwnProfile() throws IOException {
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(context), TextSecurePreferences.getLocalNumber(context));
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
if (pipe != null) {
try {
return pipe.getProfile(address, Optional.absent());
} catch (IOException e) {
Log.w(TAG, e);
}
}
return receiver.retrieveProfile(address, Optional.absent());
}
public enum UploadResult {
SUCCESS,
ERROR_FILE_IO
}
}

View File

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.profiles.edit;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.guava.Optional;
class EditProfileViewModel extends ViewModel {
private final MutableLiveData<String> givenName = new MutableLiveData<>();
private final MutableLiveData<String> familyName = new MutableLiveData<>();
private final LiveData<ProfileName> internalProfileName = Transformations.map(new LiveDataPair<>(givenName, familyName),
pair -> ProfileName.fromParts(pair.first(), pair.second()));
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
private final EditProfileRepository repository;
private EditProfileViewModel(@NonNull EditProfileRepository repository) {
this.repository = repository;
repository.getCurrentUsername(internalUsername::postValue);
repository.getCurrentProfileName(name -> {
givenName.setValue(name.getGivenName());
familyName.setValue(name.getFamilyName());
});
repository.getCurrentAvatar(internalAvatar::setValue);
}
public LiveData<ProfileName> profileName() {
return internalProfileName;
}
public LiveData<byte[]> avatar() {
return Transformations.distinctUntilChanged(internalAvatar);
}
public LiveData<Optional<String>> username() {
return internalUsername;
}
public boolean hasAvatar() {
return internalAvatar.getValue() != null;
}
public void setGivenName(String givenName) {
this.givenName.setValue(givenName);
}
public void setFamilyName(String familyName) {
this.familyName.setValue(familyName);
}
public void setAvatar(byte[] avatar) {
internalAvatar.setValue(avatar);
}
public void submitProfile(Consumer<EditProfileRepository.UploadResult> uploadResultConsumer) {
ProfileName profileName = internalProfileName.getValue();
if (profileName == null) {
return;
}
repository.uploadProfile(profileName, internalAvatar.getValue(), uploadResultConsumer);
}
private ProfileName currentProfileName() {
return internalProfileName.getValue();
}
static class Factory implements ViewModelProvider.Factory {
private final EditProfileRepository repository;
Factory(EditProfileRepository repository) {
this.repository = repository;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new EditProfileViewModel(repository);
}
}
}

View File

@ -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;
}

View File

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -43,7 +44,7 @@ public class RecipientDetails {
final boolean blocked;
final int expireMessages;
final List<Recipient> participants;
final String profileName;
final ProfileName profileName;
final Optional<Integer> defaultSubscriptionId;
final RegisteredState registered;
final byte[] profileKey;

View File

@ -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;
}

View File

@ -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();

View File

@ -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());
});
}
}

View File

@ -1,130 +0,0 @@
package org.thoughtcrime.securesms.usernames;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
public class ProfileEditOverviewFragment extends Fragment {
private ImageView avatarView;
private TextView profileText;
private TextView usernameText;
private AlertDialog loadingDialog;
private ProfileEditOverviewViewModel viewModel;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.profile_edit_overview_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
avatarView = view.findViewById(R.id.profile_overview_avatar);
profileText = view.findViewById(R.id.profile_overview_profile_name);
usernameText = view.findViewById(R.id.profile_overview_username);
View profileButton = view.findViewById(R.id.profile_overview_profile_edit_button );
View usernameButton = view.findViewById(R.id.profile_overview_username_edit_button);
TextView infoText = view.findViewById(R.id.profile_overview_info_text);
profileButton.setOnClickListener(v -> {
Navigation.findNavController(view).navigate(ProfileEditOverviewFragmentDirections.actionProfileEdit());
});
usernameButton.setOnClickListener(v -> {
Navigation.findNavController(view).navigate(ProfileEditOverviewFragmentDirections.actionUsernameEdit());
});
infoText.setMovementMethod(LinkMovementMethod.getInstance());
profileText.setOnClickListener(v -> profileButton.callOnClick());
usernameText.setOnClickListener(v -> usernameButton.callOnClick());
avatarView.setOnClickListener(v -> Permissions.with(this)
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.onAnyResult(() -> viewModel.onAvatarClicked(this))
.execute());
viewModel = ViewModelProviders.of(this, new ProfileEditOverviewViewModel.Factory()).get(ProfileEditOverviewViewModel.class);
viewModel.getAvatar().observe(getViewLifecycleOwner(), this::onAvatarChanged);
viewModel.getLoading().observe(getViewLifecycleOwner(), this::onLoadingChanged);
viewModel.getProfileName().observe(getViewLifecycleOwner(), this::onProfileNameChanged);
viewModel.getUsername().observe(getViewLifecycleOwner(), this::onUsernameChanged);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (!viewModel.onActivityResult(this, requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onResume() {
super.onResume();
viewModel.onResume();
}
private void onAvatarChanged(@NonNull Optional<byte[]> avatar) {
if (avatar.isPresent()) {
GlideApp.with(this)
.load(avatar.get())
.circleCrop()
.into(avatarView);
} else {
avatarView.setImageDrawable(null);
}
}
private void onLoadingChanged(boolean loading) {
if (loadingDialog == null && loading) {
loadingDialog = SimpleProgressDialog.show(requireContext());
} else if (loadingDialog != null) {
loadingDialog.dismiss();
loadingDialog = null;
}
}
private void onProfileNameChanged(@NonNull Optional<String> profileName) {
profileText.setText(profileName.or(""));
}
@SuppressLint("SetTextI18n")
private void onUsernameChanged(@NonNull Optional<String> username) {
if (username.isPresent()) {
usernameText.setText("@" + username.get());
} else {
usernameText.setText("");
}
}
}

View File

@ -1,171 +0,0 @@
package org.thoughtcrime.securesms.usernames;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.w3c.dom.Text;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
class ProfileEditOverviewRepository {
private static final String TAG = Log.tag(ProfileEditOverviewRepository.class);
private final Application application;
private final SignalServiceAccountManager accountManager;
private final Executor executor;
ProfileEditOverviewRepository() {
this.application = ApplicationDependencies.getApplication();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.executor = SignalExecutors.UNBOUNDED;
}
void getProfileAvatar(@NonNull Callback<Optional<byte[]>> callback) {
executor.execute(() -> callback.onResult(getProfileAvatarInternal()));
}
void setProfileAvatar(@NonNull byte[] data, @NonNull Callback<ProfileAvatarResult> callback) {
executor.execute(() -> callback.onResult(setProfileAvatarInternal(data)));
}
void deleteProfileAvatar(@NonNull Callback<ProfileAvatarResult> callback) {
executor.execute(() -> callback.onResult(deleteProfileAvatarInternal()));
}
void getProfileName(@NonNull Callback<Optional<String>> callback) {
executor.execute(() -> callback.onResult(getProfileNameInternal()));
}
void getUsername(@NonNull Callback<Optional<String>> callback) {
executor.execute(() -> callback.onResult(getUsernameInternal()));
}
@WorkerThread
private @NonNull Optional<byte[]> getProfileAvatarInternal() {
RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.getAvatarFile(application, selfId).exists() && AvatarHelper.getAvatarFile(application, selfId).length() > 0) {
try {
return Optional.of(Util.readFully(AvatarHelper.getInputStreamFor(application, selfId)));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar!", e);
return Optional.absent();
}
} else {
return Optional.absent();
}
}
@WorkerThread
private @NonNull ProfileAvatarResult setProfileAvatarInternal(@NonNull byte[] data) {
StreamDetails avatar = new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length);
try {
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(application), avatar);
AvatarHelper.setAvatar(application, Recipient.self().getId(), data);
TextSecurePreferences.setProfileAvatarId(application, new SecureRandom().nextInt());
return ProfileAvatarResult.SUCCESS;
} catch (IOException e) {
return ProfileAvatarResult.NETWORK_FAILURE;
}
}
@WorkerThread
private @NonNull ProfileAvatarResult deleteProfileAvatarInternal() {
try {
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(application), null);
AvatarHelper.delete(application, Recipient.self().getId());
TextSecurePreferences.setProfileAvatarId(application, 0);
return ProfileAvatarResult.SUCCESS;
} catch (IOException e) {
return ProfileAvatarResult.NETWORK_FAILURE;
}
}
@WorkerThread
private @NonNull Optional<String> getProfileNameInternal() {
try {
SignalServiceProfile profile = retrieveOwnProfile();
String encryptedProfileName = profile.getName();
String plaintextProfileName = null;
if (encryptedProfileName != null) {
ProfileCipher profileCipher = new ProfileCipher(ProfileKeyUtil.getProfileKey(application));
plaintextProfileName = new String(profileCipher.decryptName(Base64.decode(encryptedProfileName)));
}
TextSecurePreferences.setProfileName(application, plaintextProfileName);
DatabaseFactory.getRecipientDatabase(application).setProfileName(Recipient.self().getId(), plaintextProfileName);
} catch (IOException | InvalidCiphertextException e) {
Log.w(TAG, "Failed to retrieve profile name remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getProfileName(application));
}
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = retrieveOwnProfile();
TextSecurePreferences.setLocalUsername(application, profile.getUsername());
DatabaseFactory.getRecipientDatabase(application).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(application));
}
private SignalServiceProfile retrieveOwnProfile() throws IOException {
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(application), TextSecurePreferences.getLocalNumber(application));
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
if (pipe != null) {
try {
return pipe.getProfile(address, Optional.absent());
} catch (IOException e) {
Log.w(TAG, e);
}
}
return receiver.retrieveProfile(address, Optional.absent());
}
enum ProfileAvatarResult {
SUCCESS, NETWORK_FAILURE
}
interface Callback<E> {
void onResult(@NonNull E result);
}
}

View File

@ -1,212 +0,0 @@
package org.thoughtcrime.securesms.usernames;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
class ProfileEditOverviewViewModel extends ViewModel {
private static final String TAG = Log.tag(ProfileEditOverviewViewModel.class);
private final Application application;
private final ProfileEditOverviewRepository repo;
private final SingleLiveEvent<Event> event;
private final MutableLiveData<Optional<byte[]>> avatar;
private final MutableLiveData<Boolean> loading;
private final MutableLiveData<Optional<String>> profileName;
private final MutableLiveData<Optional<String>> username;
private File captureFile;
private ProfileEditOverviewViewModel() {
this.application = ApplicationDependencies.getApplication();
this.repo = new ProfileEditOverviewRepository();
this.avatar = new MutableLiveData<>();
this.loading = new MutableLiveData<>();
this.profileName = new MutableLiveData<>();
this.username = new MutableLiveData<>();
this.event = new SingleLiveEvent<>();
profileName.setValue(Optional.fromNullable(TextSecurePreferences.getProfileName(application)));
username.setValue(Optional.fromNullable(TextSecurePreferences.getLocalUsername(application)));
loading.setValue(false);
repo.getProfileAvatar(avatar::postValue);
repo.getProfileName(profileName::postValue);
repo.getUsername(username::postValue);
}
void onAvatarClicked(@NonNull Fragment fragment) {
//noinspection ConstantConditions Initial value is set
captureFile = AvatarSelection.startAvatarSelection(fragment, avatar.getValue().isPresent(), true);
}
boolean onActivityResult(@NonNull Fragment fragment, int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode) {
case AvatarSelection.REQUEST_CODE_AVATAR:
handleAvatarResult(fragment, resultCode, data);
return true;
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
handleCropImage(resultCode, data);
return true;
default:
return false;
}
}
void onResume() {
profileName.setValue(Optional.fromNullable(TextSecurePreferences.getProfileName(application)));
username.setValue(Optional.fromNullable(TextSecurePreferences.getLocalUsername(application)));
}
@NonNull LiveData<Optional<byte[]>> getAvatar() {
return avatar;
}
@NonNull LiveData<Boolean> getLoading() {
return loading;
}
@NonNull LiveData<Optional<String>> getProfileName() {
return profileName;
}
@NonNull LiveData<Optional<String>> getUsername() {
return username;
}
@NonNull LiveData<Event> getEvents() {
return event;
}
private void handleAvatarResult(@NonNull Fragment fragment, int resultCode, @Nullable Intent data) {
if (resultCode != Activity.RESULT_OK) {
Log.w(TAG, "Bad result for REQUEST_CODE_AVATAR.");
event.postValue(Event.IMAGE_SAVE_FAILURE);
return;
}
if (data != null && data.getBooleanExtra("delete", false)) {
Log.i(TAG, "Deleting profile avatar.");
Optional<byte[]> oldAvatar = avatar.getValue();
avatar.setValue(Optional.absent());
loading.setValue(true);
repo.deleteProfileAvatar(result -> {
switch (result) {
case SUCCESS:
loading.postValue(false);
break;
case NETWORK_FAILURE:
loading.postValue(false);
avatar.postValue(oldAvatar);
event.postValue(Event.NETWORK_ERROR);
break;
}
});
} else {
Uri outputFile = Uri.fromFile(new File(application.getCacheDir(), "cropped"));
Uri inputFile = (data != null ? data.getData() : null);
if (inputFile == null && captureFile != null) {
inputFile = Uri.fromFile(captureFile);
}
if (inputFile != null) {
AvatarSelection.circularCropImage(fragment, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
} else {
Log.w(TAG, "No input file!");
event.postValue(Event.IMAGE_SAVE_FAILURE);
}
}
}
private void handleCropImage(int resultCode, @Nullable Intent data) {
if (resultCode != Activity.RESULT_OK) {
Log.w(TAG, "Bad result for REQUEST_CODE_CROP_IMAGE.");
event.postValue(Event.IMAGE_SAVE_FAILURE);
return;
}
Optional<byte[]> oldAvatar = avatar.getValue();
loading.setValue(true);
SignalExecutors.BOUNDED.execute(() -> {
try {
BitmapUtil.ScaleResult scaled = BitmapUtil.createScaledBytes(application, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
if (captureFile != null) {
captureFile.delete();
}
avatar.postValue(Optional.of(scaled.getBitmap()));
repo.setProfileAvatar(scaled.getBitmap(), result -> {
switch (result) {
case SUCCESS:
loading.postValue(false);
break;
case NETWORK_FAILURE:
loading.postValue(false);
avatar.postValue(oldAvatar);
event.postValue(Event.NETWORK_ERROR);
break;
}
});
} catch (BitmapDecodingException e) {
event.postValue(Event.IMAGE_SAVE_FAILURE);
}
});
}
@Override
protected void onCleared() {
if (captureFile != null) {
captureFile.delete();
}
}
enum Event {
IMAGE_SAVE_FAILURE, NETWORK_ERROR
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ProfileEditOverviewViewModel());
}
}
}

View File

@ -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);
}
}

View File

@ -1,57 +0,0 @@
package org.thoughtcrime.securesms.usernames.profile;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
import java.util.concurrent.Executor;
class ProfileEditNameRepository {
private final Application application;
private final SignalServiceAccountManager accountManager;
private final Executor executor;
ProfileEditNameRepository() {
this.application = ApplicationDependencies.getApplication();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.executor = SignalExecutors.UNBOUNDED;
}
void setProfileName(@NonNull String profileName, @NonNull Callback<ProfileNameResult> callback) {
executor.execute(() -> callback.onResult(setProfileNameInternal(profileName)));
}
@WorkerThread
private @NonNull ProfileNameResult setProfileNameInternal(@NonNull String profileName) {
Util.sleep(1000);
try {
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(application), profileName);
TextSecurePreferences.setProfileName(application, profileName);
DatabaseFactory.getRecipientDatabase(application).setProfileName(Recipient.self().getId(), profileName);
return ProfileNameResult.SUCCESS;
} catch (IOException e) {
return ProfileNameResult.NETWORK_FAILURE;
}
}
enum ProfileNameResult {
SUCCESS, NETWORK_FAILURE
}
interface Callback<E> {
void onResult(@NonNull E result);
}
}

View File

@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.usernames.profile;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
class ProfileEditNameViewModel extends ViewModel {
private final ProfileEditNameRepository repo;
private final SingleLiveEvent<Event> events;
private final MutableLiveData<Boolean> loading;
private ProfileEditNameViewModel() {
this.repo = new ProfileEditNameRepository();
this.events = new SingleLiveEvent<>();
this.loading = new MutableLiveData<>();
}
void onSubmitPressed(@NonNull String profileName) {
loading.setValue(true);
repo.setProfileName(profileName, result -> {
switch (result) {
case SUCCESS:
events.postValue(Event.SUCCESS);
break;
case NETWORK_FAILURE:
events.postValue(Event.NETWORK_FAILURE);
break;
}
loading.postValue(false);
});
}
@NonNull LiveData<Event> getEvents() {
return events;
}
@NonNull LiveData<Boolean> isLoading() {
return loading;
}
enum Event {
SUCCESS, NETWORK_FAILURE
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ProfileEditNameViewModel());
}
}
}

View File

@ -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)) {

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.util.text;
import android.text.Editable;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
public final class AfterTextChanged implements TextWatcher {
private final Consumer<Editable> afterTextChangedConsumer;
public AfterTextChanged(@NonNull Consumer<Editable> afterTextChangedConsumer) {
this.afterTextChangedConsumer = afterTextChangedConsumer;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
afterTextChangedConsumer.accept(s);
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="8dp" android:left="8dp" android:right="8dp" android:bottom="8dp">
<shape android:shape="oval">
<solid android:color="@color/white" />
</shape>
</item>
</layer-list>

View File

@ -1,183 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".profiles.edit.EditProfileActivity">
<org.thoughtcrime.securesms.components.InputAwareLayout
android:id="@+id/container"
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
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" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:text="@string/CreateProfileActivity_set_up_your_profile"
app:layout_constraintBottom_toTopOf="@+id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="spread_inside" />
<ImageView
android:id="@+id/avatar_background"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginStart="32dp"
android:layout_marginTop="4dp"
android:src="@drawable/circle_tintable"
android:tint="@color/core_grey_05"
app:layout_constraintBottom_toBottomOf="@+id/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/name" />
<ImageView
android:id="@+id/avatar_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:srcCompat="@drawable/ic_profile_outline_40"
android:tint="@color/core_grey_60"
android:transitionName="avatar"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background" />
<ImageView
android:id="@+id/avatar"
android:contentDescription="@string/CreateProfileActivity_set_avatar_description"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background" />
<ImageView
android:id="@+id/camera_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="35dp"
android:layout_marginTop="35dp"
android:cropToPadding="false"
android:src="@drawable/ic_profile_camera"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:hint="@string/profile_create_activity__your_name"
app:labeledEditText_background="?attr/conversation_background"
app:labeledEditText_label="@string/CreateProfileActivity_profile_name"
app:labeledEditText_textLayout="@layout/profile_name_text"
app:layout_constraintBottom_toTopOf="@+id/description_text"
app:layout_constraintEnd_toStartOf="@+id/emoji_toggle"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/avatar_background"
app:layout_constraintTop_toBottomOf="@+id/title" />
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
android:id="@+id/emoji_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="9dp"
android:layout_marginEnd="32dp"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__emoji_toggle_description"
app:layout_constraintBottom_toBottomOf="@+id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/name" />
<TextView
android:id="@+id/description_text"
style="@style/Signal.Text.Preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/CreateProfileActivity_signal_profiles_are_end_to_end_encrypted"
android:textColor="@color/core_grey_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.dd.CircularProgressButton
android:id="@+id/finish_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:background="@color/signal_primary"
android:textAllCaps="true"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/profile_create_activity__finish" />
<Button
android:id="@+id/skip_button"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/profile_create_activity__set_later"
android:textColor="@color/core_grey_50" />
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard
android:id="@+id/emoji_drawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</org.thoughtcrime.securesms.components.InputAwareLayout>
<View
android:id="@+id/reveal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/textsecure_primary"
android:visibility="invisible"
tools:visibility="gone"/>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground">
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Signal.Title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:text="@string/CreateProfileActivity_set_up_your_profile"
app:layout_constraintBottom_toTopOf="@+id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="spread_inside" />
<ImageView
android:id="@+id/avatar_background"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginTop="16dp"
android:src="@drawable/circle_tintable"
android:tint="@color/core_grey_05"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<ImageView
android:id="@+id/avatar_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:tint="@color/core_grey_75"
android:transitionName="avatar"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background"
app:srcCompat="@drawable/ic_profile_outline_40" />
<ImageView
android:id="@+id/avatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/CreateProfileActivity_set_avatar_description"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background" />
<TextView
android:id="@+id/name_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/avatar"
tools:text="Name Preview" />
<ImageView
android:id="@+id/camera_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="56dp"
android:layout_marginTop="56dp"
android:background="@drawable/circle_tintable_padded"
android:cropToPadding="false"
android:elevation="4dp"
android:padding="14dp"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
app:layout_constraintTop_toTopOf="@+id/avatar_background"
app:srcCompat="?conversation_attach_camera" />
<LinearLayout
android:id="@+id/name_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name_preview">
<EditText
android:id="@+id/given_name"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginTop="13dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:hint="@string/CreateProfileActivity_first_name_required"
android:inputType="textPersonName"
android:maxLength="@integer/profile_name_part_max_length"
android:singleLine="true" />
<EditText
android:id="@+id/family_name"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginTop="13dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:hint="@string/CreateProfileActivity_last_name_optional"
android:inputType="textPersonName"
android:maxLength="@integer/profile_name_part_max_length"
android:singleLine="true" />
</LinearLayout>
<TextView
android:id="@+id/profile_overview_username_label"
style="@style/Signal.Text.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:text="@string/CreateProfileActivity__username"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name_container" />
<EditText
android:id="@+id/profile_overview_username"
style="@style/Signal.Text.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:background="@null"
android:editable="false"
android:focusable="false"
android:hint="@string/CreateProfileActivity__create_a_username"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/profile_overview_username_edit_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label" />
<ImageView
android:id="@+id/profile_overview_username_edit_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="8dp"
android:tint="@color/core_grey_55"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/profile_overview_username"
app:srcCompat="@drawable/ic_compose_solid_24" />
<TextView
android:id="@+id/description_text"
style="@style/Signal.Text.Preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="11dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/CreateProfileActivity_signal_profiles_are_end_to_end_encrypted"
android:textColor="@color/core_grey_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profile_overview_username_edit_button"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.dd.CircularProgressButton
android:id="@+id/finish_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:background="@color/signal_primary"
android:enabled="false"
android:textAllCaps="true"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/CreateProfileActivity_next" />
</LinearLayout>
<View
android:id="@+id/reveal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/textsecure_primary"
android:visibility="invisible"
tools:visibility="gone" />
</FrameLayout>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:titleTextAppearance="@style/TextSecure.TitleTextStyle" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/profile_edit" />
</LinearLayout>

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp">
<EditText
android:id="@+id/profile_name_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:hint="@string/ProfileEditNameFragment_profile_name"
style="@style/Signal.Text.Body"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/profile_name_description"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:text="@string/ProfileEditNameFragment_your_profile_name_can_be_seen_by_your_contacts"
style="@style/Signal.Text.Caption"
app:layout_constraintTop_toBottomOf="@id/profile_name_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/profile_name_submit"/>
<com.dd.CircularProgressButton
android:id="@+id/profile_name_submit"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/ProfileEditNameFragment_save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,159 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/circle_tintable"
android:tint="@color/core_grey_05"
app:layout_constraintTop_toTopOf="@id/profile_overview_avatar"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_avatar"
app:layout_constraintStart_toStartOf="@id/profile_overview_avatar"
app:layout_constraintEnd_toEndOf="@id/profile_overview_avatar" />
<ImageView
android:id="@+id/profile_overview_avatar_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
android:tint="@color/core_grey_60"
app:srcCompat="@drawable/ic_profile_outline_40"
app:layout_constraintTop_toTopOf="@id/profile_overview_avatar"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_avatar"
app:layout_constraintStart_toStartOf="@id/profile_overview_avatar"
app:layout_constraintEnd_toEndOf="@id/profile_overview_avatar" />
<ImageView
android:id="@+id/profile_overview_avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginTop="36dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/profile_overview_camera_button"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="50dp"
android:layout_marginTop="50dp"
android:cropToPadding="false"
android:src="@drawable/ic_profile_camera"
app:layout_constraintStart_toStartOf="@+id/profile_overview_avatar"
app:layout_constraintTop_toTopOf="@+id/profile_overview_avatar" />
<TextView
android:id="@+id/profile_overview_profile_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/ProfileEditOverviewFragment_profile_name"
style="@style/Signal.Text.Caption"
app:layout_constraintTop_toBottomOf="@id/profile_overview_avatar"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter"/>
<EditText
android:id="@+id/profile_overview_profile_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:editable="false"
android:focusable="false"
android:hint="@string/ProfileEditOverviewFragment_create_a_profile_name"
android:background="@null"
style="@style/Signal.Text.Body"
tools:text="Peter Parker"
app:layout_constraintTop_toBottomOf="@id/profile_overview_profile_label"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toStartOf="@id/profile_overview_profile_edit_button" />
<ImageView
android:id="@+id/profile_overview_profile_edit_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="8dp"
android:tint="@color/core_grey_55"
app:srcCompat="@drawable/ic_compose_solid_24"
app:layout_constraintTop_toTopOf="@id/profile_overview_profile_name"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_profile_name"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter" />
<View
android:id="@+id/profile_overview_divider"
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginTop="14dp"
android:background="@color/transparent_black_20"
app:layout_constraintTop_toBottomOf="@id/profile_overview_profile_name"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter"/>
<TextView
android:id="@+id/profile_overview_username_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/ProfileEditOverviewFragment_username"
style="@style/Signal.Text.Caption"
app:layout_constraintTop_toBottomOf="@id/profile_overview_divider"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter"/>
<EditText
android:id="@+id/profile_overview_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:editable="false"
android:focusable="false"
android:hint="@string/ProfileEditOverviewFragment_create_a_username"
android:background="@null"
style="@style/Signal.Text.Body"
app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toStartOf="@id/profile_overview_username_edit_button"/>
<ImageView
android:id="@+id/profile_overview_username_edit_button"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="8dp"
android:tint="@color/core_grey_55"
app:srcCompat="@drawable/ic_compose_solid_24"
app:layout_constraintTop_toTopOf="@id/profile_overview_username"
app:layout_constraintBottom_toBottomOf="@id/profile_overview_username"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter"/>
<TextView
android:id="@+id/profile_overview_info_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/ProfileEditOverviewFragment_your_signal_profile_can_be_seen_by"
style="@style/Signal.Text.Caption"
app:layout_constraintTop_toBottomOf="@id/profile_overview_username"
app:layout_constraintStart_toStartOf="@id/profile_overview_left_gutter"
app:layout_constraintEnd_toEndOf="@id/profile_overview_right_gutter"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/profile_overview_left_gutter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/profile_overview_right_gutter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:hint="@string/ProfileEditOverviewFragment_create_a_username"
android:hint="@string/CreateProfileActivity__create_a_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/create_profile"
app:startDestination="@id/createProfileFragment">
<fragment
android:id="@+id/createProfileFragment"
android:name="org.thoughtcrime.securesms.profiles.edit.EditProfileFragment"
android:label="fragment_create_profile"
tools:layout="@layout/profile_create_fragment">
<action
android:id="@+id/action_editUsername"
app:destination="@id/usernameEditFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/usernameEditFragment"
android:name="org.thoughtcrime.securesms.usernames.username.UsernameEditFragment"
android:label="fragment_edit_username"
tools:layout="@layout/username_edit_fragment" />
</navigation>

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/profile_edit"
app:startDestination="@id/profileEditOverviewFragment">
<fragment
android:id="@+id/profileEditOverviewFragment"
android:name="org.thoughtcrime.securesms.usernames.ProfileEditOverviewFragment"
android:label="@string/ProfileEditOverviewFragment_profile">
<action
android:id="@+id/action_profileEdit"
app:destination="@id/profileEditNameFragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
<action
android:id="@+id/action_usernameEdit"
app:destination="@id/usernameEditFragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
</fragment>
<fragment
android:id="@+id/profileEditNameFragment"
android:name="org.thoughtcrime.securesms.usernames.profile.ProfileEditNameFragment"
android:label="@string/ProfileEditNameFragment_profile_name" />
<fragment
android:id="@+id/usernameEditFragment"
android:name="org.thoughtcrime.securesms.usernames.username.UsernameEditFragment"
android:label="@string/UsernameEditFragment_username"
tools:layout="@layout/username_edit_fragment" />
</navigation>

View File

@ -4,4 +4,6 @@
<integer name="reaction_scrubber_reveal_duration">400</integer>
<integer name="reaction_scrubber_reveal_emoji_duration">380</integer>
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
<integer name="profile_name_part_max_length">26</integer>
</resources>

View File

@ -312,12 +312,9 @@
<string name="ConversationTitleView_verified">Verified</string>
<!-- CreateProfileActivity -->
<string name="CreateProfileActivity_your_profile_info">Your profile info</string>
<string name="CreateProfileActivity_error_setting_profile_photo">Error setting profile photo</string>
<string name="CreateProfileActivity_problem_setting_profile">Problem setting profile</string>
<string name="CreateProfileActivity_profile_photo">Profile photo</string>
<string name="CreateProfileActivity_too_long">Too long</string>
<string name="CreateProfileActivity_profile_name">Profile Name</string>
<string name="CreateProfileActivity_set_up_your_profile">Set up your profile</string>
<string name="CreateProfileActivity_signal_profiles_are_end_to_end_encrypted">Signal profiles are end-to-end encrypted, and the Signal service never has access to this information.</string>
<string name="CreateProfileActivity_set_avatar_description">Set avatar</string>
@ -646,19 +643,6 @@
<!-- PlayServicesProblemFragment -->
<string name="PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning">The version of Google Play Services you have installed is not functioning correctly. Please reinstall Google Play Services and try again.</string>
<!-- ProfileEditNameFragment -->
<string name="ProfileEditNameFragment_profile_name">Profile name</string>
<string name="ProfileEditNameFragment_your_profile_name_can_be_seen_by_your_contacts">Your profile name can be seen by your contacts and by other users or groups when you initiate a conversation or accept a conversation request.</string>
<string name="ProfileEditNameFragment_save">Save</string>
<!-- ProfileEditOverviewFragment -->
<string name="ProfileEditOverviewFragment_profile">Profile</string>
<string name="ProfileEditOverviewFragment_profile_name">Profile name</string>
<string name="ProfileEditOverviewFragment_username">Username</string>
<string name="ProfileEditOverviewFragment_create_a_profile_name">Create a profile name</string>
<string name="ProfileEditOverviewFragment_create_a_username">Create a username</string>
<string name="ProfileEditOverviewFragment_your_signal_profile_can_be_seen_by">Your Signal Profile can be seen by your contacts and by other users or groups when you initiate a conversation or accept a conversation request. <a href="https://support.signal.org/hc/en-us/articles/360007459591-Signal-Profiles">Tap here to learn more</a>.</string>
<!-- RatingManager -->
<string name="RatingManager_rate_this_app">Rate this app</string>
<string name="RatingManager_if_you_enjoy_using_this_app_please_take_a_moment">If you enjoy using this app, please take a moment to help us by rating it.</string>
@ -1263,10 +1247,11 @@
<string name="prompt_mms_activity__to_send_media_and_group_messages_tap_ok">To send media and group messages, tap \'OK\' and complete the requested settings. The MMS settings for your carrier can generally be located by searching for \'your carrier APN\'. You will only need to do this once.</string>
<!-- profile_create_activity -->
<string name="profile_create_activity__set_later">Set later</string>
<string name="profile_create_activity__finish">FINISH</string>
<string name="profile_create_activity__who_can_see_this_information">Who can see this information?</string>
<string name="profile_create_activity__your_name">Your name</string>
<string name="CreateProfileActivity_first_name_required">First name (required)</string>
<string name="CreateProfileActivity_last_name_optional">Last name (optional)</string>
<string name="CreateProfileActivity_next">Next</string>
<string name="CreateProfileActivity__username">Username</string>
<string name="CreateProfileActivity__create_a_username">Create a username</string>
<!-- recipient_preferences_activity -->
<string name="recipient_preference_activity__shared_media">Shared media</string>

View File

@ -0,0 +1,164 @@
package org.thoughtcrime.securesms.profiles;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class ProfileNameTest {
@Test
public void givenEmpty_thenIExpectSaneDefaults() {
// GIVEN
ProfileName profileName = ProfileName.EMPTY;
// THEN
assertNotNull("ProfileName should be non-null", profileName);
assertFalse("ProfileName should not be CJKV", profileName.isProfileNameCJKV());
assertEquals("ProfileName should have empty given name", "", profileName.getGivenName());
assertEquals("ProfileName should have empty family name", "", profileName.getFamilyName());
}
@Test
public void givenNullProfileName_whenIFromDataString_thenIExpectSaneDefaults() {
// GIVEN
ProfileName profileName = ProfileName.fromSerialized(null);
// THEN
assertSame(ProfileName.EMPTY, profileName);
}
@Test
public void givenProfileNameWithGivenNameOnly_whenIFromDataString_thenIExpectValidProfileName() {
// GIVEN
String profileName = "Given";
// WHEN
ProfileName name = ProfileName.fromSerialized(profileName);
// THEN
assertNotNull("ProfileName should be non-null", name);
assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
assertEquals("ProfileName should have expected given name", profileName, name.getGivenName());
assertEquals("ProfileName should have empty family name", "", name.getFamilyName());
}
@Test
public void givenProfileNameWithEnglishGivenNameAndEnglishFamilyName_whenIFromDataString_thenIExpectValidProfileName() {
// GIVEN
String profileName = "Given\0Family";
// WHEN
ProfileName name = ProfileName.fromSerialized(profileName);
// THEN
assertNotNull("ProfileName should be non-null", name);
assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
assertEquals("ProfileName should have expected given name", "Given", name.getGivenName());
assertEquals("ProfileName should have expected family name", "Family", name.getFamilyName());
}
@Test
public void givenProfileNameWithEnglishGivenNameAndCJKVFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
// GIVEN
String profileName = "Given\0码";
// WHEN
ProfileName name = ProfileName.fromSerialized(profileName);
// THEN
assertNotNull("ProfileName should be non-null", name);
assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
assertEquals("ProfileName should have expected given name", "Given", name.getGivenName());
assertEquals("ProfileName should have expected family name", "码", name.getFamilyName());
}
@Test
public void givenProfileNameWithCJKVGivenNameAndCJKVFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
// GIVEN
String profileName = "统\0码";
// WHEN
ProfileName name = ProfileName.fromSerialized(profileName);
// THEN
assertNotNull("ProfileName should be non-null", name);
assertTrue("ProfileName should be CJKV", name.isProfileNameCJKV());
assertEquals("ProfileName should have expected given name", "统", name.getGivenName());
assertEquals("ProfileName should have expected family name", "码", name.getFamilyName());
}
@Test
public void givenProfileNameWithCJKVGivenNameAndEnglishFamilyName_whenIFromDataString_thenIExpectNonCJKVProfileName() {
// GIVEN
String profileName = "统\0Family";
// WHEN
ProfileName name = ProfileName.fromSerialized(profileName);
// THEN
assertNotNull("ProfileName should be non-null", name);
assertFalse("ProfileName should not be CJKV", name.isProfileNameCJKV());
assertEquals("ProfileName should have expected given name", "统", name.getGivenName());
assertEquals("ProfileName should have expected family name", "Family", name.getFamilyName());
}
@Test
public void givenProfileNameWithEmptyInputs_whenIToDataString_thenIExpectAnEmptyString() {
// GIVEN
ProfileName name = ProfileName.fromParts("", "");
// WHEN
String data = name.serialize();
// THEN
assertEquals("Blank String should be returned (For back compat)", "", data);
}
@Test
public void givenProfileNameWithEmptyGivenName_whenIToDataString_thenIExpectAnEmptyString() {
// GIVEN
ProfileName name = ProfileName.fromParts("", "Family");
// WHEN
String data = name.serialize();
// THEN
assertEquals("Blank String should be returned (For back compat)", "", data);
}
@Test
public void givenProfileNameWithGivenName_whenIToDataString_thenIExpectValidProfileName() {
// GIVEN
ProfileName name = ProfileName.fromParts("Given", "");
// WHEN
String data = name.serialize();
// THEN
assertEquals(data, "Given\0");
}
@Test
public void givenProfileNameWithGivenNameAndFamilyName_whenIToDataString_thenIExpectValidProfileName() {
// GIVEN
ProfileName name = ProfileName.fromParts("Given", "Family");
// WHEN
String data = name.serialize();
// THEN
assertEquals(data, "Given\0Family");
}
@Test
public void fromParts_with_long_name_parts() {
ProfileName name = ProfileName.fromParts("GivenSomeVeryLongNameSomeVeryLongName", "FamilySomeVeryLongNameSomeVeryLongName");
assertEquals("GivenSomeVeryLongNameSomeV", name.getGivenName());
assertEquals("FamilySomeVeryLongNameSome", name.getFamilyName());
}
}

View File

@ -8,6 +8,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.whispersystems.libsignal.util.guava.Optional;
import static android.provider.ContactsContract.Intents.Insert.NAME;
@ -24,7 +25,7 @@ public final class RecipientExporterTest {
@Test
public void asAddContactIntent_with_phone_number() {
Recipient recipient = givenPhoneRecipient("Alice", "+1555123456");
Recipient recipient = givenPhoneRecipient(ProfileName.fromParts("Alice", null), "+1555123456");
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
@ -37,7 +38,7 @@ public final class RecipientExporterTest {
@Test
public void asAddContactIntent_with_email() {
Recipient recipient = givenEmailRecipient("Bob", "bob@signal.org");
Recipient recipient = givenEmailRecipient(ProfileName.fromParts("Bob", null), "bob@signal.org");
Intent intent = RecipientExporter.export(recipient).asAddContactIntent();
@ -48,7 +49,7 @@ public final class RecipientExporterTest {
assertNull(intent.getStringExtra(PHONE));
}
private Recipient givenPhoneRecipient(String profileName, String phone) {
private Recipient givenPhoneRecipient(ProfileName profileName, String phone) {
Recipient recipient = mock(Recipient.class);
when(recipient.getProfileName()).thenReturn(profileName);
@ -59,7 +60,7 @@ public final class RecipientExporterTest {
return recipient;
}
private Recipient givenEmailRecipient(String profileName, String email) {
private Recipient givenEmailRecipient(ProfileName profileName, String email) {
Recipient recipient = mock(Recipient.class);
when(recipient.getProfileName()).thenReturn(profileName);

View File

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.util.cjkv;
import android.app.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static org.junit.Assert.*;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public class CJKVUtilTest {
private static final String CJKV_CHARS = "统码";
private static final String NON_CJKV_CHAR = "a";
private static final String MIXED_CHARS = CJKV_CHARS + NON_CJKV_CHAR;
@Test
public void givenAllCJKVChars_whenIsCJKV_thenIExpectTrue() {
// WHEN
boolean result = CJKVUtil.isCJKV(CJKV_CHARS);
//THEN
assertTrue(result);
}
@Test
public void givenNoCJKVChars_whenIsCJKV_thenIExpectFalse() {
// WHEN
boolean result = CJKVUtil.isCJKV(NON_CJKV_CHAR);
// THEN
assertFalse(result);
}
@Test
public void givenOneNonCJKVChar_whenIsCJKV_thenIExpectFalse() {
// WHEN
boolean result = CJKVUtil.isCJKV(MIXED_CHARS);
// THEN
assertFalse(result);
}
@Test
public void givenAnEmptyString_whenIsCJKV_thenIExpectTrue() {
// WHEN
boolean result = CJKVUtil.isCJKV("");
// THEN
assertTrue(result);
}
@Test
public void givenNull_whenIsCJKV_thenIExpectTrue() {
// WHEN
boolean result = CJKVUtil.isCJKV(null);
// THEN
assertTrue(result);
}
}

View File

@ -19,7 +19,7 @@ import javax.crypto.spec.SecretKeySpec;
public class ProfileCipher {
public static final int NAME_PADDED_LENGTH = 26;
public static final int NAME_PADDED_LENGTH = 53;
private final byte[] key;

View File

@ -19,9 +19,9 @@ public class ProfileCipherTest extends TestCase {
public void testEncryptDecrypt() throws InvalidCiphertextException {
byte[] key = Util.getSecretBytes(32);
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encryptName("Clement Duval".getBytes(), 26);
byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
byte[] plaintext = cipher.decryptName(name);
assertEquals(new String(plaintext), "Clement Duval");
assertEquals(new String(plaintext), "Clement\0Duval");
}
public void testEmpty() throws Exception {