Add support for setting an optional last name in profiles.

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

View File

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

View File

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

View File

@@ -7,12 +7,13 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.viewpager.widget.ViewPager;
import android.view.View;
import com.melnykov.fab.FloatingActionButton;
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ServiceUtil;
@@ -71,7 +73,7 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity
R.string.ExperienceUpgradeActivity_signal_profiles_are_here,
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
CreateProfileActivity.class,
EditProfileActivity.class,
false),
READ_RECEIPTS(299,
new IntroPage(0xFF2090EA,

View File

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

View File

@@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
@@ -63,8 +61,8 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self));
} else if (!FeatureFlags.PROFILE_DISPLAY && recipient.getName(getContext()) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") ");
} else if (!FeatureFlags.PROFILE_DISPLAY && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@@ -20,7 +20,6 @@ package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -40,7 +39,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -384,8 +382,8 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientForeverObs
} else {
this.name.setText(recipient.getName(getContext()));
if (recipient.getName(getContext()) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
this.phoneNumber.setText(recipient.requireE164() + " (~" + recipient.getProfileName() + ")");
if (recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
this.phoneNumber.setText(recipient.requireE164() + " (~" + recipient.getProfileName().toString() + ")");
} else {
this.phoneNumber.setText(recipient.requireE164());
}

View File

@@ -55,7 +55,7 @@ public class ContactRepository {
add(new Pair<>(NAME_COLUMN, cursor -> {
String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME));
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SIGNAL_PROFILE_NAME));
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SEARCH_PROFILE_NAME));
return Util.getFirstNonEmpty(system, profile);
}));
@@ -96,7 +96,7 @@ public class ContactRepository {
boolean shouldAdd = !nameMatch && !numberMatch;
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION);
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES);
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle });
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });

View File

@@ -1,18 +1,13 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.database.StorageKeyDatabase;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SetUtil;
@@ -219,7 +214,7 @@ public final class StorageSyncHelper {
return new SignalContactRecord.Builder(storageKey, new SignalServiceAddress(recipient.getUuid(), recipient.getE164()))
.setProfileKey(recipient.getProfileKey())
.setProfileName(recipient.getProfileName())
.setProfileName(recipient.getProfileName().serialize())
.setBlocked(recipient.isBlocked())
.setProfileSharingEnabled(recipient.isProfileSharing())
.setIdentityKey(recipient.getIdentityKey())

View File

@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.thoughtcrime.securesms.util.cjkv.CJKVUtil;
import static org.thoughtcrime.securesms.contactshare.Contact.*;
public class ContactNameEditViewModel extends ViewModel {
@@ -67,7 +69,12 @@ public class ContactNameEditViewModel extends ViewModel {
}
private String buildDisplayName() {
boolean isCJKV = isCJKV(givenName) && isCJKV(middleName) && isCJKV(familyName) && isCJKV(prefix) && isCJKV(suffix);
boolean isCJKV = CJKVUtil.isCJKV(givenName) &&
CJKVUtil.isCJKV(middleName) &&
CJKVUtil.isCJKV(familyName) &&
CJKVUtil.isCJKV(prefix) &&
CJKVUtil.isCJKV(suffix);
if (isCJKV) {
return joinString(familyName, givenName, prefix, suffix, middleName);
}
@@ -86,47 +93,4 @@ public class ContactNameEditViewModel extends ViewModel {
return builder.toString().trim();
}
private boolean isCJKV(@Nullable String value) {
if (TextUtils.isEmpty(value)) {
return true;
}
for (int offset = 0; offset < value.length(); ) {
int codepoint = Character.codePointAt(value, offset);
if (!isCodepointCJKV(codepoint)) {
return false;
}
offset += Character.charCount(codepoint);
}
return true;
}
private boolean isCodepointCJKV(int codepoint) {
if (codepoint == (int)' ') return true;
Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT.equals(block) ||
Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT.equals(block) ||
Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(block) ||
Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS.equals(block) ||
Character.UnicodeBlock.KANGXI_RADICALS.equals(block) ||
Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS.equals(block) ||
Character.UnicodeBlock.HIRAGANA.equals(block) ||
Character.UnicodeBlock.KATAKANA.equals(block) ||
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS.equals(block) ||
Character.UnicodeBlock.HANGUL_JAMO.equals(block) ||
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO.equals(block) ||
Character.UnicodeBlock.HANGUL_SYLLABLES.equals(block) ||
Character.isIdeographic(codepoint);
}
}

View File

@@ -966,8 +966,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} else {
this.groupSender.setText(recipient.toShortString(context));
if (recipient.getName(context) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
this.groupSenderProfileName.setText("~" + recipient.getProfileName());
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
this.groupSenderProfileName.setText("~" + recipient.getProfileName().toString());
this.groupSenderProfileName.setVisibility(View.VISIBLE);
} else {
this.groupSenderProfileName.setText(null);

View File

@@ -142,10 +142,10 @@ public class ConversationTitleView extends RelativeLayout {
private void setNonContactRecipientTitle(Recipient recipient) {
this.title.setText(Util.getFirstNonEmpty(recipient.getE164().orNull(), recipient.getUuid().transform(UUID::toString).orNull()));
if (TextUtils.isEmpty(recipient.getProfileName())) {
if (recipient.getProfileName().isEmpty()) {
this.subtitle.setText(null);
} else {
this.subtitle.setText("~" + recipient.getProfileName());
this.subtitle.setText("~" + recipient.getProfileName().toString());
}
updateSubtitleVisibility();

View File

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

View File

@@ -9,10 +9,10 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import com.bumptech.glide.Glide;
@@ -20,7 +20,6 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@@ -47,7 +46,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
@@ -102,8 +100,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int RESUMABLE_DOWNLOADS = 40;
private static final int KEY_VALUE_STORE = 41;
private static final int ATTACHMENT_DISPLAY_ORDER = 42;
private static final int SPLIT_PROFILE_NAMES = 43;
private static final int DATABASE_VERSION = 42;
private static final int DATABASE_VERSION = 43;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -531,11 +530,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
values.put("phone", localNumber);
values.put("registered", 1);
values.put("profile_sharing", 1);
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context));
values.put("signal_profile_name", TextSecurePreferences.getProfileName(context).getGivenName());
db.insert("recipient", null, values);
} else {
db.execSQL("UPDATE recipient SET registered = ?, profile_sharing = ?, signal_profile_name = ? WHERE phone = ?",
new String[] { "1", "1", TextSecurePreferences.getProfileName(context), localNumber });
new String[] { "1", "1", TextSecurePreferences.getProfileName(context).getGivenName(), localNumber });
}
}
}
@@ -699,6 +698,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN display_order INTEGER DEFAULT 0");
}
if (oldVersion < SPLIT_PROFILE_NAMES) {
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_family_name TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_joined_name TEXT DEFAULT NULL");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

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

View File

@@ -96,6 +96,7 @@ public final class JobManagerFactories {
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(MarkerJob.KEY, new MarkerJob.Factory());
put(Argon2TestJob.KEY, new Argon2TestJob.Factory());
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
// Migrations
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());

View File

@@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
public final class ProfileUploadJob extends BaseJob {
public static final String KEY = "ProfileUploadJob";
private final Context context;
private final SignalServiceAccountManager accountManager;
public ProfileUploadJob() {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(Parameters.IMMORTAL)
.setMaxAttempts(Parameters.UNLIMITED)
.setMaxInstances(1)
.build());
}
private ProfileUploadJob(@NonNull Parameters parameters) {
super(parameters);
this.context = ApplicationDependencies.getApplication();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
}
@Override
protected void onRun() throws Exception {
uploadProfileName();
uploadAvatar();
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return true;
}
@Override
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onFailure() {
}
private void uploadProfileName() throws Exception {
ProfileName profileName = TextSecurePreferences.getProfileName(context);
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(context), profileName.serialize());
}
private void uploadAvatar() throws Exception {
final RecipientId selfId = Recipient.self().getId();
final byte[] avatar;
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
avatar = Util.readFully(AvatarHelper.getInputStreamFor(context, Recipient.self().getId()));
} else {
avatar = null;
}
final StreamDetails avatarDetails;
if (avatar == null || avatar.length == 0) {
avatarDetails = null;
} else {
avatarDetails = new StreamDetails(new ByteArrayInputStream(avatar),
MediaUtil.IMAGE_JPEG,
avatar.length);
}
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(context), avatarDetails);
}
public static class Factory implements Job.Factory {
@NonNull
@Override
public Job create(@NonNull Parameters parameters, @NonNull Data data) {
return new ProfileUploadJob(parameters);
}
}
}

View File

@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -74,11 +75,12 @@ public class RefreshOwnProfileJob extends BaseJob {
private void setProfileName(@Nullable String encryptedName) {
try {
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), plaintextName);
TextSecurePreferences.setProfileName(context, plaintextName);
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
TextSecurePreferences.setProfileName(context, profileName);
} catch (InvalidCiphertextException | IOException e) {
Log.w(TAG, e);
}

View File

@@ -6,8 +6,6 @@ import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@@ -16,28 +14,19 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.IOException;
import java.util.List;
@@ -191,9 +180,9 @@ public class RetrieveProfileJob extends BaseJob {
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);
if (!Util.equals(plaintextProfileName, recipient.getProfileName())) {
if (!Util.equals(plaintextProfileName, recipient.getProfileName().serialize())) {
Log.i(TAG, "Profile name updated. Writing new value.");
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), plaintextProfileName);
DatabaseFactory.getRecipientDatabase(context).setProfileName(recipient.getId(), ProfileName.fromSerialized(plaintextProfileName));
}
if (TextUtils.isEmpty(plaintextProfileName)) {

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
@@ -52,7 +51,7 @@ public class RotateProfileKeyJob extends BaseJob {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] profileKey = ProfileKeyUtil.rotateProfileKey(context);
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context));
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());

View File

@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class ProfilePreference extends Preference {
@@ -66,7 +65,7 @@ public class ProfilePreference extends Preference {
if (profileSubtextView == null) return;
final Recipient self = Recipient.self();
final String profileName = TextSecurePreferences.getProfileName(getContext());
final String profileName = TextSecurePreferences.getProfileName(getContext()).toString();
GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(self.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(getContext()))))

View File

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

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.annotation.SuppressLint;
import android.os.Bundle;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@SuppressLint("StaticFieldLeak")
public class EditProfileActivity extends BaseActionBarActivity implements EditProfileFragment.Controller {
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
dynamicTheme.onCreate(this);
setContentView(R.layout.profile_create_activity);
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, getIntent().getExtras());
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public void onProfileNameUploadCompleted() {
finish();
}
}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
@@ -34,9 +33,9 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
@@ -83,7 +82,7 @@ public class Recipient {
private final Uri systemContactPhoto;
private final String customLabel;
private final Uri contactUri;
private final String profileName;
private final ProfileName profileName;
private final String profileAvatar;
private final boolean profileSharing;
private final String notificationChannel;
@@ -295,7 +294,7 @@ public class Recipient {
this.systemContactPhoto = null;
this.customLabel = null;
this.contactUri = null;
this.profileName = null;
this.profileName = ProfileName.EMPTY;
this.profileAvatar = null;
this.profileSharing = false;
this.notificationChannel = null;
@@ -383,7 +382,7 @@ public class Recipient {
public @NonNull String getDisplayName(@NonNull Context context) {
return Util.getFirstNonEmpty(getName(context),
getProfileName(),
getProfileName().toString(),
getDisplayUsername(),
e164,
email,
@@ -518,7 +517,7 @@ public class Recipient {
return defaultSubscriptionId;
}
public @Nullable String getProfileName() {
public @NonNull ProfileName getProfileName() {
return profileName;
}

View File

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

View File

@@ -21,7 +21,8 @@ public final class RecipientExporter {
public Intent asAddContactIntent() {
Intent intent = new Intent(ACTION_INSERT_OR_EDIT);
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
addNameToIntent(intent, recipient.getProfileName());
addNameToIntent(intent, recipient.getProfileName().toString());
addAddressToIntent(intent, recipient);
return intent;
}

View File

@@ -12,9 +12,9 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@@ -31,8 +31,7 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (!isReregister()) {
// TODO [greyson] Navigation
activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, MainActivity.class)));
activity.startActivity(getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class)));
}
activity.finish();

View File

@@ -1,64 +0,0 @@
package org.thoughtcrime.securesms.usernames;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.navigation.ActivityNavigator;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class ProfileEditActivityV2 extends PassphraseRequiredActionBarActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent getLaunchIntent(@NonNull Context context) {
return new Intent(context, ProfileEditActivityV2.class);
}
@Override
protected void onPreCreate() {
super.onPreCreate();
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.profile_edit_activity_v2);
initToolbar();
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private void initToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(v -> onBackPressed());
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
getSupportActionBar().setTitle(destination.getLabel());
});
}
}

View File

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

View File

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

View File

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

View File

@@ -1,79 +0,0 @@
package org.thoughtcrime.securesms.usernames.profile;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.fragment.NavHostFragment;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class ProfileEditNameFragment extends Fragment {
private EditText profileText;
private CircularProgressButton submitButton;
private ProfileEditNameViewModel viewModel;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.profile_edit_name_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
profileText = view.findViewById(R.id.profile_name_text);
submitButton = view.findViewById(R.id.profile_name_submit);
viewModel = ViewModelProviders.of(this, new ProfileEditNameViewModel.Factory()).get(ProfileEditNameViewModel.class);
viewModel.isLoading().observe(getViewLifecycleOwner(), this::onLoadingChanged);
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
profileText.setText(TextSecurePreferences.getProfileName(requireContext()));
submitButton.setOnClickListener(v -> viewModel.onSubmitPressed(profileText.getText().toString()));
}
private void onLoadingChanged(boolean loading) {
if (loading) {
profileText.setEnabled(false);
setSpinning(submitButton);
} else {
profileText.setEnabled(true);
cancelSpinning(submitButton);
}
}
private void onEvent(@NonNull ProfileEditNameViewModel.Event event) {
switch (event) {
case SUCCESS:
Toast.makeText(requireContext(), R.string.ProfileEditNameFragment_successfully_set_profile_name, Toast.LENGTH_SHORT).show();
NavHostFragment.findNavController(this).popBackStack();
break;
case NETWORK_FAILURE:
Toast.makeText(requireContext(), R.string.ProfileEditNameFragment_encountered_a_network_error, Toast.LENGTH_SHORT).show();
break;
}
}
private static void setSpinning(@NonNull CircularProgressButton button) {
button.setClickable(false);
button.setIndeterminateProgressMode(true);
button.setProgress(50);
}
private static void cancelSpinning(@NonNull CircularProgressButton button) {
button.setProgress(0);
button.setIndeterminateProgressMode(false);
button.setClickable(true);
}
}

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ public final class AvatarUtil {
}
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
String name = Optional.fromNullable(recipient.getDisplayName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
String name = Optional.fromNullable(recipient.getDisplayName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context).toString())).or("");
MaterialColor fallbackColor = recipient.getColor();
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -442,12 +443,12 @@ public class TextSecurePreferences {
setStringPreference(context, PROFILE_KEY_PREF, key);
}
public static void setProfileName(Context context, String name) {
setStringPreference(context, PROFILE_NAME_PREF, name);
public static void setProfileName(Context context, ProfileName name) {
setStringPreference(context, PROFILE_NAME_PREF, name.serialize());
}
public static String getProfileName(Context context) {
return getStringPreference(context, PROFILE_NAME_PREF, null);
public static ProfileName getProfileName(Context context) {
return ProfileName.fromSerialized(getStringPreference(context, PROFILE_NAME_PREF, null));
}
public static void setProfileAvatarId(Context context, int id) {

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.util.cjkv;
import androidx.annotation.Nullable;
public final class CJKVUtil {
private CJKVUtil() {
}
public static boolean isCJKV(@Nullable String value) {
if (value == null || value.length() == 0) {
return true;
}
for (int offset = 0; offset < value.length(); ) {
int codepoint = Character.codePointAt(value, offset);
if (!isCodepointCJKV(codepoint)) {
return false;
}
offset += Character.charCount(codepoint);
}
return true;
}
private static boolean isCodepointCJKV(int codepoint) {
if (codepoint == (int)' ') return true;
Character.UnicodeBlock block = Character.UnicodeBlock.of(codepoint);
return Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS.equals(block) ||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A.equals(block) ||
Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS.equals(block) ||
Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT.equals(block) ||
Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT.equals(block) ||
Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION.equals(block) ||
Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS.equals(block) ||
Character.UnicodeBlock.KANGXI_RADICALS.equals(block) ||
Character.UnicodeBlock.IDEOGRAPHIC_DESCRIPTION_CHARACTERS.equals(block) ||
Character.UnicodeBlock.HIRAGANA.equals(block) ||
Character.UnicodeBlock.KATAKANA.equals(block) ||
Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS.equals(block) ||
Character.UnicodeBlock.HANGUL_JAMO.equals(block) ||
Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO.equals(block) ||
Character.UnicodeBlock.HANGUL_SYLLABLES.equals(block) ||
Character.isIdeographic(codepoint);
}
}

View File

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