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