mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-14 05:53:19 +01:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e551ea8bd9 | ||
|
|
5a28b1bf1c | ||
|
|
4cd1129c92 | ||
|
|
a5d7bc4efc | ||
|
|
1ff5b2af2a | ||
|
|
82446ce30a | ||
|
|
6465248483 | ||
|
|
48e7f82466 | ||
|
|
6fef21ebc0 | ||
|
|
837e594607 | ||
|
|
ab0cb55b80 | ||
|
|
2d24c8c525 | ||
|
|
40383f3733 | ||
|
|
e14861d79d | ||
|
|
b29b3d0432 | ||
|
|
c21d4861c0 | ||
|
|
a6786e5c2b | ||
|
|
77caa9e9d4 | ||
|
|
835ef02872 | ||
|
|
279dcb1428 | ||
|
|
4a8c312e0a | ||
|
|
c2bc376f87 | ||
|
|
73160d4d26 | ||
|
|
1dd2a4e9c5 | ||
|
|
ed0c4b8de5 | ||
|
|
4f921d761d | ||
|
|
37f85d6deb | ||
|
|
e1b75c78ab | ||
|
|
5e83206e6e | ||
|
|
1ea6838db6 | ||
|
|
fb82420376 | ||
|
|
109d67956f | ||
|
|
9f7b2e2cfd | ||
|
|
22f9bfeceb | ||
|
|
ef4c7e96da | ||
|
|
02865f99a9 | ||
|
|
ef6019f13b | ||
|
|
33d02bb7b8 | ||
|
|
d34df2c1cf | ||
|
|
7fdf540742 | ||
|
|
f916aabb98 | ||
|
|
4ae7d56db4 | ||
|
|
e3878ffde7 | ||
|
|
5221b6fb43 | ||
|
|
5e0fe86858 | ||
|
|
c86ced0911 | ||
|
|
0aad82d3d7 |
@@ -188,8 +188,8 @@ dependencyVerification {
|
||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 594
|
||||
def canonicalVersionName = "4.54.1"
|
||||
def canonicalVersionCode = 598
|
||||
def canonicalVersionName = "4.55.1"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
|
||||
@@ -414,11 +414,21 @@
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".lock.v2.KbsMigrationActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ClearProfileAvatarActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo">
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"/>
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -155,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,6 +250,8 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.registrationValues().onNewInstall();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ public class BasicIntroFragment extends Fragment {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getArguments() != null) {
|
||||
drawable = getArguments().getInt(ARG_DRAWABLE);
|
||||
text = getArguments().getInt(ARG_TEXT );
|
||||
subtext = getArguments().getInt(ARG_SUBTEXT );
|
||||
text = getArguments().getInt(ARG_TEXT);
|
||||
subtext = getArguments().getInt(ARG_SUBTEXT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,14 @@ import androidx.fragment.app.Fragment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
@@ -35,6 +40,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||
private static final int STATE_EXPERIENCE_UPGRADE = 4;
|
||||
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||
private static final int STATE_CREATE_KBS_PIN = 7;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -150,6 +157,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
||||
case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent();
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -165,11 +174,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
||||
return STATE_EXPERIENCE_UPGRADE;
|
||||
} else if (userMustSetKbsPin()) {
|
||||
return STATE_CREATE_KBS_PIN;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean userMustSetKbsPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !PinUtil.userHasPin(this);
|
||||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && TextSecurePreferences.getProfileName(this) == ProfileName.EMPTY;
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
|
||||
}
|
||||
@@ -193,6 +214,22 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||
}
|
||||
|
||||
private Intent getCreateKbsPinIntent() {
|
||||
|
||||
final Intent intent;
|
||||
if (userMustSetProfileName()) {
|
||||
intent = getCreateProfileNameIntent();
|
||||
} else {
|
||||
intent = getIntent();
|
||||
}
|
||||
|
||||
return getRoutedIntent(CreateKbsPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getCreateProfileNameIntent() {
|
||||
return getRoutedIntent(EditProfileActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
public final class AnimationRepeatListener implements Animator.AnimatorListener {
|
||||
|
||||
private final Consumer<Animator> animationConsumer;
|
||||
|
||||
public AnimationRepeatListener(@NonNull Consumer<Animator> animationConsumer) {
|
||||
this.animationConsumer = animationConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationStart(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationEnd(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationRepeat(Animator animation) {
|
||||
this.animationConsumer.accept(animation);
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,25 @@ package org.thoughtcrime.securesms.backup;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
public class BackupDialog {
|
||||
|
||||
@@ -83,4 +89,35 @@ public class BackupDialog {
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
|
||||
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.BackupDialog_verify, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButton.setEnabled(false);
|
||||
|
||||
RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter();
|
||||
|
||||
prompt.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
formatter.afterTextChanged(editable);
|
||||
positiveButton.setEnabled(editable.length() == BackupUtil.PASSPHRASE_LENGTH);
|
||||
}));
|
||||
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
String passphrase = prompt.getText().toString();
|
||||
if (passphrase.equals(BackupPassphrase.get(context))) {
|
||||
Toast.makeText(context, R.string.BackupDialog_you_successfully_entered_your_backup_passphrase, Toast.LENGTH_SHORT).show();
|
||||
dialog.dismiss();
|
||||
} else {
|
||||
Toast.makeText(context, R.string.BackupDialog_passphrase_was_not_correct, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class AttachmentTypeSelector extends PopupWindow {
|
||||
|
||||
public static final int ADD_GALLERY = 1;
|
||||
public static final int ADD_DOCUMENT = 2;
|
||||
public static final int ADD_SOUND = 3;
|
||||
public static final int ADD_CONTACT_INFO = 4;
|
||||
public static final int TAKE_PHOTO = 5;
|
||||
public static final int ADD_LOCATION = 6;
|
||||
public static final int ADD_GIF = 7;
|
||||
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
|
||||
|
||||
private final @NonNull LoaderManager loaderManager;
|
||||
private final @NonNull RecentPhotoViewRail recentRail;
|
||||
private final @NonNull ImageView imageButton;
|
||||
private final @NonNull ImageView audioButton;
|
||||
private final @NonNull ImageView documentButton;
|
||||
private final @NonNull ImageView contactButton;
|
||||
private final @NonNull ImageView cameraButton;
|
||||
private final @NonNull ImageView locationButton;
|
||||
private final @NonNull ImageView gifButton;
|
||||
private final @NonNull ImageView closeButton;
|
||||
|
||||
private @Nullable View currentAnchor;
|
||||
private @Nullable AttachmentClickedListener listener;
|
||||
|
||||
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) {
|
||||
super(context);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
|
||||
|
||||
this.listener = listener;
|
||||
this.loaderManager = loaderManager;
|
||||
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
|
||||
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
|
||||
this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
|
||||
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
|
||||
this.contactButton = ViewUtil.findById(layout, R.id.contact_button);
|
||||
this.cameraButton = ViewUtil.findById(layout, R.id.camera_button);
|
||||
this.locationButton = ViewUtil.findById(layout, R.id.location_button);
|
||||
this.gifButton = ViewUtil.findById(layout, R.id.giphy_button);
|
||||
this.closeButton = ViewUtil.findById(layout, R.id.close_button);
|
||||
|
||||
this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY));
|
||||
this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND));
|
||||
this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT));
|
||||
this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO));
|
||||
this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO));
|
||||
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
|
||||
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
|
||||
this.closeButton.setOnClickListener(new CloseClickListener());
|
||||
this.recentRail.setListener(new RecentPhotoSelectedListener());
|
||||
|
||||
setContentView(layout);
|
||||
setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
setBackgroundDrawable(new BitmapDrawable());
|
||||
setAnimationStyle(0);
|
||||
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
|
||||
setFocusable(true);
|
||||
setTouchable(true);
|
||||
|
||||
loaderManager.initLoader(1, null, recentRail);
|
||||
}
|
||||
|
||||
public void show(@NonNull Activity activity, final @NonNull View anchor) {
|
||||
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
recentRail.setVisibility(View.VISIBLE);
|
||||
loaderManager.restartLoader(1, null, recentRail);
|
||||
} else {
|
||||
recentRail.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.currentAnchor = anchor;
|
||||
|
||||
showAtLocation(anchor, Gravity.BOTTOM, 0, 0);
|
||||
|
||||
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowInCircular(anchor, getContentView());
|
||||
} else {
|
||||
animateWindowInTranslate(getContentView());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateButtonIn(imageButton, ANIMATION_DURATION / 2);
|
||||
animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
|
||||
|
||||
animateButtonIn(audioButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(locationButton, ANIMATION_DURATION / 3);
|
||||
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(gifButton, ANIMATION_DURATION / 4);
|
||||
animateButtonIn(contactButton, 0);
|
||||
animateButtonIn(closeButton, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowOutCircular(currentAnchor, getContentView());
|
||||
} else {
|
||||
animateWindowOutTranslate(getContentView());
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@Nullable AttachmentClickedListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private void animateButtonIn(View button, int delay) {
|
||||
AnimationSet animation = new AnimationSet(true);
|
||||
Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f);
|
||||
|
||||
animation.addAnimation(scale);
|
||||
animation.setInterpolator(new OvershootInterpolator(1));
|
||||
animation.setDuration(ANIMATION_DURATION);
|
||||
animation.setStartOffset(delay);
|
||||
button.startAnimation(animation);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
0,
|
||||
Math.max(contentView.getWidth(), contentView.getHeight()));
|
||||
animator.setDuration(ANIMATION_DURATION);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private void animateWindowInTranslate(@NonNull View contentView) {
|
||||
Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0);
|
||||
animation.setDuration(ANIMATION_DURATION);
|
||||
|
||||
getContentView().startAnimation(animation);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
|
||||
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
|
||||
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
Math.max(getContentView().getWidth(), getContentView().getHeight()),
|
||||
0);
|
||||
|
||||
animator.setDuration(ANIMATION_DURATION);
|
||||
animator.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
AttachmentTypeSelector.super.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
}
|
||||
});
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private void animateWindowOutTranslate(@NonNull View contentView) {
|
||||
Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight());
|
||||
animation.setDuration(ANIMATION_DURATION);
|
||||
animation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
AttachmentTypeSelector.super.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
});
|
||||
|
||||
getContentView().startAnimation(animation);
|
||||
}
|
||||
|
||||
private Pair<Integer, Integer> getClickOrigin(@Nullable View anchor, @NonNull View contentView) {
|
||||
if (anchor == null) return new Pair<>(0, 0);
|
||||
|
||||
final int[] anchorCoordinates = new int[2];
|
||||
anchor.getLocationOnScreen(anchorCoordinates);
|
||||
anchorCoordinates[0] += anchor.getWidth() / 2;
|
||||
anchorCoordinates[1] += anchor.getHeight() / 2;
|
||||
|
||||
final int[] contentCoordinates = new int[2];
|
||||
contentView.getLocationOnScreen(contentCoordinates);
|
||||
|
||||
int x = anchorCoordinates[0] - contentCoordinates[0];
|
||||
int y = anchorCoordinates[1] - contentCoordinates[1];
|
||||
|
||||
return new Pair<>(x, y);
|
||||
}
|
||||
|
||||
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
|
||||
animateWindowOutTranslate(getContentView());
|
||||
|
||||
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size);
|
||||
}
|
||||
}
|
||||
|
||||
private class PropagatingClickListener implements View.OnClickListener {
|
||||
|
||||
private final int type;
|
||||
|
||||
private PropagatingClickListener(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
animateWindowOutTranslate(getContentView());
|
||||
|
||||
if (listener != null) listener.onClick(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CloseClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
public interface AttachmentClickedListener {
|
||||
void onClick(int type);
|
||||
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -21,20 +25,30 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
||||
|
||||
public OutlinedThumbnailView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
init(null);
|
||||
}
|
||||
|
||||
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
setRadius(0);
|
||||
|
||||
int radius = 0;
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0);
|
||||
radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0);
|
||||
}
|
||||
|
||||
setRadius(radius);
|
||||
setCorners(radius, radius, radius, radius);
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -344,6 +344,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
|
||||
return setImageResource(glideRequests, uri, 0, 0);
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
@@ -352,6 +356,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade());
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
request = request.override(width, height);
|
||||
}
|
||||
|
||||
if (radius > 0) {
|
||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
private final boolean scaleEmojis;
|
||||
private final boolean forceCustom;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
|
||||
@@ -49,6 +50,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
|
||||
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
|
||||
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
|
||||
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
|
||||
@@ -166,7 +168,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
private boolean useSystemEmoji() {
|
||||
return TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
||||
return !forceCustom && TextSecurePreferences.isSystemEmojiPreferred(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.InputView {
|
||||
|
||||
private AttachmentKeyboardMediaAdapter mediaAdapter;
|
||||
private AttachmentKeyboardButtonAdapter buttonAdapter;
|
||||
private Callback callback;
|
||||
|
||||
private RecyclerView mediaList;
|
||||
private View permissionText;
|
||||
private View permissionButton;
|
||||
|
||||
public AttachmentKeyboard(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public AttachmentKeyboard(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(@NonNull Context context) {
|
||||
inflate(context, R.layout.attachment_keyboard, this);
|
||||
|
||||
this.mediaList = findViewById(R.id.attachment_keyboard_media_list );
|
||||
this.permissionText = findViewById(R.id.attachment_keyboard_permission_text );
|
||||
this.permissionButton = findViewById(R.id.attachment_keyboard_permission_button);
|
||||
|
||||
RecyclerView buttonList = findViewById(R.id.attachment_keyboard_button_list);
|
||||
|
||||
mediaAdapter = new AttachmentKeyboardMediaAdapter(GlideApp.with(this), media -> {
|
||||
if (callback != null) {
|
||||
callback.onAttachmentMediaClicked(media);
|
||||
}
|
||||
});
|
||||
|
||||
buttonAdapter = new AttachmentKeyboardButtonAdapter(button -> {
|
||||
if (callback != null) {
|
||||
callback.onAttachmentSelectorClicked(button);
|
||||
}
|
||||
});
|
||||
|
||||
mediaList.setAdapter(mediaAdapter);
|
||||
buttonList.setAdapter(buttonAdapter);
|
||||
|
||||
mediaList.setLayoutManager(new GridLayoutManager(context, 1, GridLayoutManager.HORIZONTAL, false));
|
||||
buttonList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
|
||||
|
||||
buttonAdapter.setButtons(Arrays.asList(
|
||||
AttachmentKeyboardButton.GALLERY,
|
||||
AttachmentKeyboardButton.GIF,
|
||||
AttachmentKeyboardButton.FILE,
|
||||
AttachmentKeyboardButton.CONTACT,
|
||||
AttachmentKeyboardButton.LOCATION
|
||||
));
|
||||
}
|
||||
|
||||
public void setCallback(@NonNull Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void onMediaChanged(@NonNull List<Media> media) {
|
||||
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
mediaAdapter.setMedia(media);
|
||||
permissionButton.setVisibility(GONE);
|
||||
permissionText.setVisibility(GONE);
|
||||
} else {
|
||||
permissionButton.setVisibility(VISIBLE);
|
||||
permissionText.setVisibility(VISIBLE);
|
||||
|
||||
permissionButton.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.onAttachmentPermissionsRequested();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(int height, boolean immediate) {
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
params.height = height;
|
||||
setLayoutParams(params);
|
||||
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(boolean immediate) {
|
||||
setVisibility(GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShowing() {
|
||||
return getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onAttachmentMediaClicked(@NonNull Media media);
|
||||
void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button);
|
||||
void onAttachmentPermissionsRequested();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public enum AttachmentKeyboardButton {
|
||||
|
||||
GALLERY(R.string.AttachmentKeyboard_gallery, R.drawable.ic_photo_album_outline_32),
|
||||
GIF(R.string.AttachmentKeyboard_gif, R.drawable.ic_gif_outline_32),
|
||||
FILE(R.string.AttachmentKeyboard_file, R.drawable.ic_file_outline_32),
|
||||
CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.ic_contact_circle_outline_32),
|
||||
LOCATION(R.string.AttachmentKeyboard_location, R.drawable.ic_location_outline_32);
|
||||
|
||||
private final int titleRes;
|
||||
private final int iconRes;
|
||||
|
||||
AttachmentKeyboardButton(@StringRes int titleRes, @DrawableRes int iconRes) {
|
||||
this.titleRes = titleRes;
|
||||
this.iconRes = iconRes;
|
||||
}
|
||||
|
||||
public @StringRes int getTitleRes() {
|
||||
return titleRes;
|
||||
}
|
||||
|
||||
public @DrawableRes int getIconRes() {
|
||||
return iconRes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
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.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKeyboardButtonAdapter.ButtonViewHolder> {
|
||||
|
||||
private final List<AttachmentKeyboardButton> buttons;
|
||||
private final Listener listener;
|
||||
|
||||
AttachmentKeyboardButtonAdapter(@NonNull Listener listener) {
|
||||
this.buttons = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return buttons.get(position).getTitleRes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ButtonViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboard_button_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) {
|
||||
holder.bind(buttons.get(position), listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ButtonViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return buttons.size();
|
||||
}
|
||||
|
||||
|
||||
public void setButtons(@NonNull List<AttachmentKeyboardButton> buttons) {
|
||||
this.buttons.clear();
|
||||
this.buttons.addAll(buttons);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onClick(@NonNull AttachmentKeyboardButton button);
|
||||
}
|
||||
|
||||
static class ButtonViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
private final TextView title;
|
||||
|
||||
public ButtonViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.image = itemView.findViewById(R.id.attachment_button_image);
|
||||
this.title = itemView.findViewById(R.id.attachment_button_title);
|
||||
}
|
||||
|
||||
void bind(@NonNull AttachmentKeyboardButton button, @NonNull Listener listener) {
|
||||
image.setImageResource(button.getIconRes());
|
||||
title.setText(button.getTitleRes());
|
||||
|
||||
itemView.setOnClickListener(v -> listener.onClick(button));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.OutlinedThumbnailView;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyboardMediaAdapter.MediaViewHolder> {
|
||||
|
||||
private final List<Media> media;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Listener listener;
|
||||
private final StableIdGenerator<Media> idGenerator;
|
||||
|
||||
AttachmentKeyboardMediaAdapter(@NonNull GlideRequests glideRequests, @NonNull Listener listener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.listener = listener;
|
||||
this.media = new ArrayList<>();
|
||||
this.idGenerator = new StableIdGenerator<>();
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return idGenerator.getId(media.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new MediaViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboad_media_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
holder.bind(media.get(position), glideRequests, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return media.size();
|
||||
}
|
||||
|
||||
public void setMedia(@NonNull List<Media> media) {
|
||||
this.media.clear();
|
||||
this.media.addAll(media);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onMediaClicked(@NonNull Media media);
|
||||
}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final OutlinedThumbnailView image;
|
||||
private final TextView duration;
|
||||
private final View videoIcon;
|
||||
|
||||
public MediaViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
image = itemView.findViewById(R.id.attachment_keyboard_item_image);
|
||||
duration = itemView.findViewById(R.id.attachment_keyboard_item_video_time);
|
||||
videoIcon = itemView.findViewById(R.id.attachment_keyboard_item_video_icon);
|
||||
}
|
||||
|
||||
void bind(@NonNull Media media, @NonNull GlideRequests glideRequests, @NonNull Listener listener) {
|
||||
image.setImageResource(glideRequests, media.getUri(), 400, 400);
|
||||
image.setOnClickListener(v -> listener.onMediaClicked(media));
|
||||
|
||||
duration.setVisibility(View.GONE);
|
||||
videoIcon.setVisibility(View.GONE);
|
||||
|
||||
if (media.getDuration() > 0) {
|
||||
duration.setVisibility(View.VISIBLE);
|
||||
duration.setText(formatTime(media.getDuration()));
|
||||
} else if (MediaUtil.isVideoType(media.getMimeType())) {
|
||||
videoIcon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
image.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@NonNull static String formatTime(long time) {
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(time);
|
||||
time -= TimeUnit.HOURS.toMillis(hours);
|
||||
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(time);
|
||||
time -= TimeUnit.MINUTES.toHours(time);
|
||||
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(time);
|
||||
|
||||
if (hours > 0) {
|
||||
return zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds);
|
||||
} else {
|
||||
return zeroPad(minutes) + ":" + zeroPad(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull static String zeroPad(long value) {
|
||||
if (value < 10) {
|
||||
return "0" + value;
|
||||
} else {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,6 @@ import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
|
||||
import org.thoughtcrime.securesms.components.HidingLinearLayout;
|
||||
@@ -262,7 +261,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
InputPanel.MediaListener,
|
||||
ComposeText.CursorPositionChangedListener,
|
||||
ConversationSearchBottomBar.EventListener,
|
||||
StickerKeyboardProvider.StickerEventListener
|
||||
StickerKeyboardProvider.StickerEventListener,
|
||||
AttachmentKeyboard.Callback
|
||||
{
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
@@ -311,18 +311,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private FrameLayout messageRequestOverlay;
|
||||
private ConversationReactionOverlay reactionOverlay;
|
||||
|
||||
private AttachmentTypeSelector attachmentTypeSelector;
|
||||
private AttachmentManager attachmentManager;
|
||||
private AudioRecorder audioRecorder;
|
||||
private BroadcastReceiver securityUpdateReceiver;
|
||||
private Stub<MediaKeyboard> emojiDrawerStub;
|
||||
protected HidingLinearLayout quickAttachmentToggle;
|
||||
protected HidingLinearLayout inlineAttachmentToggle;
|
||||
private InputPanel inputPanel;
|
||||
private AttachmentManager attachmentManager;
|
||||
private AudioRecorder audioRecorder;
|
||||
private BroadcastReceiver securityUpdateReceiver;
|
||||
private Stub<MediaKeyboard> emojiDrawerStub;
|
||||
private Stub<AttachmentKeyboard> attachmentKeyboardStub;
|
||||
protected HidingLinearLayout quickAttachmentToggle;
|
||||
protected HidingLinearLayout inlineAttachmentToggle;
|
||||
private InputPanel inputPanel;
|
||||
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private ConversationSearchViewModel searchViewModel;
|
||||
private ConversationStickerViewModel stickerViewModel;
|
||||
private ConversationViewModel viewModel;
|
||||
private InviteReminderModel inviteReminderModel;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
@@ -391,6 +392,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeLinkPreviewObserver();
|
||||
initializeSearchObserver();
|
||||
initializeStickerObserver();
|
||||
initializeViewModel();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -844,7 +846,45 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
//////// Event Handlers
|
||||
@Override
|
||||
public void onAttachmentMediaClicked(@NonNull Media media) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
container.hideCurrentInput(composeText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) {
|
||||
switch (button) {
|
||||
case GALLERY:
|
||||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
break;
|
||||
case GIF:
|
||||
AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getColor().toConversationColor(this));
|
||||
break;
|
||||
case FILE:
|
||||
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
|
||||
break;
|
||||
case CONTACT:
|
||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT);
|
||||
break;
|
||||
case LOCATION:
|
||||
AttachmentManager.selectLocation(this, PICK_LOCATION);
|
||||
break;
|
||||
}
|
||||
|
||||
container.hideCurrentInput(composeText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachmentPermissionsRequested() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
|
||||
.execute();
|
||||
}
|
||||
|
||||
//////// Event Handlers
|
||||
|
||||
private void handleSelectMessageExpiration() {
|
||||
if (isPushGroupConversation() && !isActiveGroup()) {
|
||||
@@ -1168,10 +1208,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void handleAddAttachment() {
|
||||
if (this.isMmsEnabled || isSecureText) {
|
||||
if (attachmentTypeSelector == null) {
|
||||
attachmentTypeSelector = new AttachmentTypeSelector(this, getSupportLoaderManager(), new AttachmentTypeListener());
|
||||
viewModel.getRecentMedia().removeObservers(this);
|
||||
|
||||
if (attachmentKeyboardStub.resolved() && container.isInputOpen() && container.getCurrentInput() == attachmentKeyboardStub.get()) {
|
||||
container.showSoftkey(composeText);
|
||||
} else {
|
||||
viewModel.getRecentMedia().observe(this, media -> attachmentKeyboardStub.get().onMediaChanged(media));
|
||||
attachmentKeyboardStub.get().setCallback(this);
|
||||
container.show(composeText, attachmentKeyboardStub.get());
|
||||
|
||||
viewModel.onAttachmentKeyboardOpen();
|
||||
}
|
||||
attachmentTypeSelector.show(this, attachButton);
|
||||
} else {
|
||||
handleManualMmsRequired();
|
||||
}
|
||||
@@ -1564,6 +1611,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
|
||||
charactersLeft = ViewUtil.findById(this, R.id.space_left);
|
||||
emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub);
|
||||
attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub);
|
||||
unblockButton = ViewUtil.findById(this, R.id.unblock_button);
|
||||
makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
|
||||
registerButton = ViewUtil.findById(this, R.id.register_button);
|
||||
@@ -1586,10 +1634,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel.setListener(this);
|
||||
inputPanel.setMediaListener(this);
|
||||
|
||||
attachmentTypeSelector = null;
|
||||
attachmentManager = new AttachmentManager(this, this);
|
||||
audioRecorder = new AudioRecorder(this);
|
||||
typingTextWatcher = new TypingStatusTextWatcher();
|
||||
attachmentManager = new AttachmentManager(this, this);
|
||||
audioRecorder = new AudioRecorder(this);
|
||||
typingTextWatcher = new TypingStatusTextWatcher();
|
||||
|
||||
SendButtonListener sendButtonListener = new SendButtonListener();
|
||||
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
|
||||
@@ -1732,6 +1779,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
this.viewModel = ViewModelProviders.of(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||
}
|
||||
|
||||
private void showStickerIntroductionTooltip() {
|
||||
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
||||
inputPanel.setMediaKeyboardToggleMode(true);
|
||||
@@ -1835,28 +1886,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
//////// Helper Methods
|
||||
|
||||
private void addAttachment(int type) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
|
||||
Log.i(TAG, "Selected: " + type);
|
||||
switch (type) {
|
||||
case AttachmentTypeSelector.ADD_GALLERY:
|
||||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()); break;
|
||||
case AttachmentTypeSelector.ADD_DOCUMENT:
|
||||
AttachmentManager.selectDocument(this, PICK_DOCUMENT); break;
|
||||
case AttachmentTypeSelector.ADD_SOUND:
|
||||
AttachmentManager.selectAudio(this, PICK_AUDIO); break;
|
||||
case AttachmentTypeSelector.ADD_CONTACT_INFO:
|
||||
AttachmentManager.selectContactInfo(this, PICK_CONTACT); break;
|
||||
case AttachmentTypeSelector.ADD_LOCATION:
|
||||
AttachmentManager.selectLocation(this, PICK_LOCATION); break;
|
||||
case AttachmentTypeSelector.TAKE_PHOTO:
|
||||
attachmentManager.capturePhoto(this, TAKE_PHOTO); break;
|
||||
case AttachmentTypeSelector.ADD_GIF:
|
||||
AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getColor().toConversationColor(this)); break;
|
||||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
|
||||
return setMedia(uri, mediaType, 0, 0);
|
||||
}
|
||||
@@ -1870,7 +1899,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
openContactShareEditor(uri);
|
||||
return new SettableFuture<>(false);
|
||||
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
|
||||
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent());
|
||||
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, 0, Optional.absent(), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
return new SettableFuture<>(false);
|
||||
} else {
|
||||
@@ -2583,7 +2612,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, Optional.absent(), Optional.absent());
|
||||
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, Optional.absent(), Optional.absent());
|
||||
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
startActivityForResult(intent, MEDIA_SENDER);
|
||||
return;
|
||||
@@ -2610,20 +2639,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Listeners
|
||||
|
||||
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
|
||||
@Override
|
||||
public void onClick(int type) {
|
||||
addAttachment(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
Media media = new Media(uri, mimeType, dateTaken, width, height, size, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
}
|
||||
}
|
||||
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -602,6 +602,7 @@ public class ConversationFragment extends Fragment
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
attachment.getSize(),
|
||||
0,
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption())));
|
||||
}
|
||||
@@ -994,8 +995,7 @@ public class ConversationFragment extends Fragment
|
||||
|
||||
if (actionMode != null) return;
|
||||
|
||||
if (FeatureFlags.reactionSending() &&
|
||||
messageRecord.isSecure() &&
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -97,6 +98,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
@@ -158,7 +160,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ViewGroup container;
|
||||
protected ViewGroup reactionsContainer;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
|
||||
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
@@ -171,7 +173,6 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private Stub<StickerView> stickerStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
private ConversationItemReactionBubbles conversationItemReactionBubbles;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
private int measureCalls;
|
||||
@@ -226,9 +227,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.container = findViewById(R.id.container);
|
||||
this.reply = findViewById(R.id.reply_icon);
|
||||
this.reactionsContainer = findViewById(R.id.reactions_bubbles_container);
|
||||
|
||||
this.conversationItemReactionBubbles = new ConversationItemReactionBubbles(this.reactionsContainer);
|
||||
this.reactionsView = findViewById(R.id.reactions_view);
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
@@ -906,8 +905,23 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull MessageRecord current) {
|
||||
conversationItemReactionBubbles.setReactions(current.getReactions());
|
||||
reactionsContainer.setOnClickListener(v -> {
|
||||
if (current.getReactions().isEmpty()) {
|
||||
reactionsView.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
bodyBubble.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
setReactionsWithWidth(current);
|
||||
bodyBubble.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setReactionsWithWidth(@NonNull MessageRecord current) {
|
||||
reactionsView.setReactions(current.getReactions(), bodyBubble.getWidth());
|
||||
reactionsView.setOnClickListener(v -> {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class ConversationItemReactionBubbles {
|
||||
|
||||
private final ViewGroup reactionsContainer;
|
||||
private final EmojiImageView primaryEmojiReaction;
|
||||
private final EmojiImageView secondaryEmojiReaction;
|
||||
|
||||
ConversationItemReactionBubbles(@NonNull ViewGroup reactionsContainer) {
|
||||
this.reactionsContainer = reactionsContainer;
|
||||
this.primaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_primary);
|
||||
this.secondaryEmojiReaction = reactionsContainer.findViewById(R.id.reactions_bubbles_secondary);
|
||||
}
|
||||
|
||||
void setReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
if (reactions.size() == 0) {
|
||||
hideAllReactions();
|
||||
return;
|
||||
}
|
||||
|
||||
final Collection<ReactionInfo> reactionInfos = getReactionInfos(reactions);
|
||||
|
||||
if (reactionInfos.size() == 1) {
|
||||
displaySingleReaction(reactionInfos.iterator().next());
|
||||
} else {
|
||||
displayMultipleReactions(reactionInfos);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Collection<ReactionInfo> getReactionInfos(@NonNull List<ReactionRecord> reactions) {
|
||||
final Map<String, ReactionInfo> counters = new HashMap<>();
|
||||
|
||||
for (ReactionRecord reaction : reactions) {
|
||||
|
||||
ReactionInfo info = counters.get(reaction.getEmoji());
|
||||
if (info == null) {
|
||||
info = new ReactionInfo(reaction.getEmoji(),
|
||||
1,
|
||||
reaction.getDateReceived(),
|
||||
Recipient.self().getId().equals(reaction.getAuthor()));
|
||||
} else {
|
||||
info = new ReactionInfo(reaction.getEmoji(),
|
||||
info.count + 1,
|
||||
Math.max(info.lastSeen, reaction.getDateReceived()),
|
||||
info.userWasSender || Recipient.self().getId().equals(reaction.getAuthor()));
|
||||
}
|
||||
|
||||
counters.put(reaction.getEmoji(), info);
|
||||
}
|
||||
|
||||
return counters.values();
|
||||
}
|
||||
|
||||
private void hideAllReactions() {
|
||||
reactionsContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void displaySingleReaction(@NonNull ReactionInfo reactionInfo) {
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
secondaryEmojiReaction.setVisibility(View.GONE);
|
||||
|
||||
primaryEmojiReaction.setImageEmoji(reactionInfo.emoji);
|
||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(reactionInfo));
|
||||
}
|
||||
|
||||
private void displayMultipleReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
||||
reactionsContainer.setVisibility(View.VISIBLE);
|
||||
primaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
secondaryEmojiReaction.setVisibility(View.VISIBLE);
|
||||
|
||||
Pair<ReactionInfo, ReactionInfo> primaryAndSecondaryReactions = getPrimaryAndSecondaryReactions(reactionInfos);
|
||||
primaryEmojiReaction.setImageEmoji(primaryAndSecondaryReactions.first.emoji);
|
||||
primaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.first));
|
||||
secondaryEmojiReaction.setImageEmoji(primaryAndSecondaryReactions.second.emoji);
|
||||
secondaryEmojiReaction.setBackground(getBackgroundDrawableForReactionBubble(primaryAndSecondaryReactions.second));
|
||||
}
|
||||
|
||||
private Drawable getBackgroundDrawableForReactionBubble(@NonNull ReactionInfo reactionInfo) {
|
||||
return ThemeUtil.getThemedDrawable(reactionsContainer.getContext(),
|
||||
reactionInfo.userWasSender ? R.attr.reactions_sent_background : R.attr.reactions_recv_background);
|
||||
}
|
||||
|
||||
private Pair<ReactionInfo, ReactionInfo> getPrimaryAndSecondaryReactions(@NonNull Collection<ReactionInfo> reactionInfos) {
|
||||
ReactionInfo mostPopular = null;
|
||||
ReactionInfo latestReaction = null;
|
||||
ReactionInfo secondLatestReaction = null;
|
||||
ReactionInfo ourReaction = null;
|
||||
|
||||
for (ReactionInfo current : reactionInfos) {
|
||||
if (current.userWasSender) {
|
||||
ourReaction = current;
|
||||
}
|
||||
|
||||
if (mostPopular == null) {
|
||||
mostPopular = current;
|
||||
} else if (mostPopular.count < current.count) {
|
||||
mostPopular = current;
|
||||
}
|
||||
|
||||
if (latestReaction == null) {
|
||||
latestReaction = current;
|
||||
} else if (latestReaction.lastSeen < current.lastSeen) {
|
||||
|
||||
if (current.count == mostPopular.count) {
|
||||
mostPopular = current;
|
||||
}
|
||||
|
||||
secondLatestReaction = latestReaction;
|
||||
latestReaction = current;
|
||||
} else if (secondLatestReaction == null) {
|
||||
secondLatestReaction = current;
|
||||
}
|
||||
}
|
||||
|
||||
if (mostPopular == null) {
|
||||
throw new AssertionError("getPrimaryAndSecondaryReactions was called with an empty list.");
|
||||
}
|
||||
|
||||
if (ourReaction != null && !mostPopular.equals(ourReaction)) {
|
||||
return Pair.create(mostPopular, ourReaction);
|
||||
} else {
|
||||
return Pair.create(mostPopular, mostPopular.equals(latestReaction) ? secondLatestReaction : latestReaction);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactionInfo {
|
||||
private final String emoji;
|
||||
private final int count;
|
||||
private final long lastSeen;
|
||||
private final boolean userWasSender;
|
||||
|
||||
private ReactionInfo(@NonNull String emoji, int count, long lastSeen, boolean userWasSender) {
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
this.lastSeen = lastSeen;
|
||||
this.userWasSender = userWasSender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(emoji, count, lastSeen, userWasSender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof ReactionInfo)) return false;
|
||||
|
||||
ReactionInfo other = (ReactionInfo) obj;
|
||||
|
||||
return other.emoji.equals(emoji) &&
|
||||
other.count == count &&
|
||||
other.lastSeen == lastSeen &&
|
||||
other.userWasSender == userWasSender;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ final class ConversationSwipeAnimationHelper {
|
||||
float progress = dx / TRIGGER_DX;
|
||||
|
||||
updateBodyBubbleTransition(conversationItem.bodyBubble, dx, sign);
|
||||
updateReactionsTransition(conversationItem.reactionsContainer, dx, sign);
|
||||
updateReactionsTransition(conversationItem.reactionsView, dx, sign);
|
||||
updateReplyIconTransition(conversationItem.reply, dx, progress, sign);
|
||||
updateContactPhotoHolderTransition(conversationItem.contactPhotoHolder, progress, sign);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class ConversationViewModel extends ViewModel {
|
||||
|
||||
private final Context context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
|
||||
private ConversationViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.mediaRepository = new MediaRepository();
|
||||
this.recentMedia = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Media>> getRecentMedia() {
|
||||
return recentMedia;
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,35 +29,10 @@ import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.MenuRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -68,6 +43,30 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.MenuRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@@ -76,8 +75,6 @@ import org.thoughtcrime.securesms.MainFragment;
|
||||
import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
|
||||
@@ -93,9 +90,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -108,8 +103,13 @@ import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneListener;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
@@ -137,7 +137,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
ActionMode.Callback,
|
||||
ItemClickListener,
|
||||
ConversationListSearchAdapter.EventListener,
|
||||
MainNavigator.BackHandler
|
||||
MainNavigator.BackHandler,
|
||||
MegaphoneListener
|
||||
{
|
||||
private static final String TAG = Log.tag(ConversationListFragment.class);
|
||||
|
||||
@@ -163,6 +164,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
private ConversationListAdapter defaultAdapter;
|
||||
private ConversationListSearchAdapter searchAdapter;
|
||||
private StickyHeaderDecoration searchAdapterDecoration;
|
||||
private ViewGroup megaphoneContainer;
|
||||
|
||||
public static ConversationListFragment newInstance() {
|
||||
return new ConversationListFragment();
|
||||
@@ -181,16 +183,17 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
reminderView = view.findViewById(R.id.reminder);
|
||||
list = view.findViewById(R.id.list);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
emptyState = view.findViewById(R.id.empty_state);
|
||||
emptyImage = view.findViewById(R.id.empty);
|
||||
searchEmptyState = view.findViewById(R.id.search_no_results);
|
||||
searchToolbar = view.findViewById(R.id.search_toolbar);
|
||||
searchAction = view.findViewById(R.id.search_action);
|
||||
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
|
||||
reminderView = view.findViewById(R.id.reminder);
|
||||
list = view.findViewById(R.id.list);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
cameraFab = view.findViewById(R.id.camera_fab);
|
||||
emptyState = view.findViewById(R.id.empty_state);
|
||||
emptyImage = view.findViewById(R.id.empty);
|
||||
searchEmptyState = view.findViewById(R.id.search_no_results);
|
||||
searchToolbar = view.findViewById(R.id.search_toolbar);
|
||||
searchAction = view.findViewById(R.id.search_action);
|
||||
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
|
||||
megaphoneContainer = view.findViewById(R.id.megaphone_container);
|
||||
|
||||
Toolbar toolbar = view.findViewById(getToolbarRes());
|
||||
toolbar.setVisibility(View.VISIBLE);
|
||||
@@ -226,7 +229,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
initializeSearchListener();
|
||||
|
||||
RatingManager.showRatingDialogIfNecessary(requireContext());
|
||||
RegistrationLockDialog.showReminderIfNecessary(requireContext());
|
||||
|
||||
RegistrationLockDialog.showReminderIfNecessary(this);
|
||||
|
||||
TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
|
||||
}
|
||||
@@ -303,6 +307,14 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
|
||||
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
|
||||
@@ -339,6 +351,31 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneNavigationRequested(@NonNull Intent intent) {
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode) {
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneToastRequested(@NonNull String string) {
|
||||
Snackbar.make(fab, string, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneSnooze(@NonNull Megaphone megaphone) {
|
||||
viewModel.onMegaphoneSnoozed(megaphone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneCompleted(@NonNull Megaphone megaphone) {
|
||||
viewModel.onMegaphoneCompleted(megaphone.getEvent());
|
||||
}
|
||||
|
||||
private void initializeProfileIcon(@NonNull Recipient recipient) {
|
||||
ImageView icon = requireView().findViewById(R.id.toolbar_icon);
|
||||
|
||||
@@ -406,19 +443,54 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory()).get(ConversationListViewModel.class);
|
||||
|
||||
viewModel.getSearchResult().observe(this, result -> {
|
||||
result = result != null ? result : SearchResult.EMPTY;
|
||||
searchAdapter.updateResults(result);
|
||||
viewModel.getSearchResult().observe(this, this::onSearchResultChanged);
|
||||
viewModel.getMegaphone().observe(this, this::onMegaphoneChanged);
|
||||
|
||||
if (result.isEmpty() && activeAdapter == searchAdapter) {
|
||||
searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
|
||||
searchEmptyState.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
searchEmptyState.setVisibility(View.GONE);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
viewModel.onVisible();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSearchResultChanged(@Nullable SearchResult result) {
|
||||
result = result != null ? result : SearchResult.EMPTY;
|
||||
searchAdapter.updateResults(result);
|
||||
|
||||
if (result.isEmpty() && activeAdapter == searchAdapter) {
|
||||
searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
|
||||
searchEmptyState.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
searchEmptyState.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
|
||||
if (megaphone == null) {
|
||||
megaphoneContainer.setVisibility(View.GONE);
|
||||
megaphoneContainer.removeAllViews();
|
||||
return;
|
||||
}
|
||||
|
||||
View view = MegaphoneViewBuilder.build(requireContext(), megaphone, this);
|
||||
|
||||
megaphoneContainer.removeAllViews();
|
||||
|
||||
if (view != null) {
|
||||
megaphoneContainer.addView(view);
|
||||
megaphoneContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
megaphoneContainer.setVisibility(View.GONE);
|
||||
|
||||
if (megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, this);
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onMegaphoneVisible(megaphone);
|
||||
}
|
||||
|
||||
private void updateReminders() {
|
||||
Context context = requireContext();
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -21,19 +24,23 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer debouncer;
|
||||
private final ContentObserver observer;
|
||||
|
||||
private String lastQuery;
|
||||
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository) {
|
||||
this.application = application;
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.searchRepository = searchRepository;
|
||||
this.debouncer = new Debouncer(300);
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
this.application = application;
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.searchRepository = searchRepository;
|
||||
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
|
||||
this.debouncer = new Debouncer(300);
|
||||
this.observer = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
if (!TextUtils.isEmpty(getLastQuery())) {
|
||||
@@ -49,6 +56,28 @@ class ConversationListViewModel extends ViewModel {
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Megaphone> getMegaphone() {
|
||||
return megaphone;
|
||||
}
|
||||
|
||||
void onVisible() {
|
||||
megaphoneRepository.getNextMegaphone(megaphone::postValue);
|
||||
}
|
||||
|
||||
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
|
||||
megaphone.postValue(null);
|
||||
megaphoneRepository.markFinished(event);
|
||||
}
|
||||
|
||||
void onMegaphoneSnoozed(@NonNull Megaphone snoozed) {
|
||||
megaphoneRepository.markSeen(snoozed.getEvent());
|
||||
megaphone.postValue(null);
|
||||
}
|
||||
|
||||
void onMegaphoneVisible(@NonNull Megaphone visible) {
|
||||
megaphoneRepository.markVisible(visible.getEvent());
|
||||
}
|
||||
|
||||
void updateQuery(String query) {
|
||||
lastQuery = query;
|
||||
debouncer.publish(() -> searchRepository.query(query, result -> {
|
||||
|
||||
@@ -61,6 +61,7 @@ public class DatabaseFactory {
|
||||
private final StickerDatabase stickerDatabase;
|
||||
private final StorageKeyDatabase storageKeyDatabase;
|
||||
private final KeyValueDatabase keyValueDatabase;
|
||||
private final MegaphoneDatabase megaphoneDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
synchronized (lock) {
|
||||
@@ -155,6 +156,10 @@ public class DatabaseFactory {
|
||||
return getInstance(context).keyValueDatabase;
|
||||
}
|
||||
|
||||
public static MegaphoneDatabase getMegaphoneDatabase(Context context) {
|
||||
return getInstance(context).megaphoneDatabase;
|
||||
}
|
||||
|
||||
public static SQLiteDatabase getBackupDatabase(Context context) {
|
||||
return getInstance(context).databaseHelper.getReadableDatabase();
|
||||
}
|
||||
@@ -193,6 +198,7 @@ public class DatabaseFactory {
|
||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
|
||||
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
|
||||
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* IMPORTANT: Writes should only be made through {@link org.thoughtcrime.securesms.megaphone.MegaphoneRepository}.
|
||||
*/
|
||||
public class MegaphoneDatabase extends Database {
|
||||
|
||||
private static final String TAG = Log.tag(MegaphoneDatabase.class);
|
||||
|
||||
private static final String TABLE_NAME = "megaphone";
|
||||
|
||||
private static final String ID = "_id";
|
||||
private static final String EVENT = "event";
|
||||
private static final String SEEN_COUNT = "seen_count";
|
||||
private static final String LAST_SEEN = "last_seen";
|
||||
private static final String FIRST_VISIBLE = "first_visible";
|
||||
private static final String FINISHED = "finished";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
EVENT + " TEXT UNIQUE, " +
|
||||
SEEN_COUNT + " INTEGER, " +
|
||||
LAST_SEEN + " INTEGER, " +
|
||||
FIRST_VISIBLE + " INTEGER, " +
|
||||
FINISHED + " INTEGER)";
|
||||
|
||||
MegaphoneDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void insert(@NonNull Collection<Event> events) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (Event event : events) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(EVENT, event.getKey());
|
||||
|
||||
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<MegaphoneRecord> getAllAndDeleteMissing() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
List<MegaphoneRecord> records = new ArrayList<>();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
Set<String> missingKeys = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String event = cursor.getString(cursor.getColumnIndexOrThrow(EVENT));
|
||||
int seenCount = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_COUNT));
|
||||
long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(LAST_SEEN));
|
||||
long firstVisible = cursor.getLong(cursor.getColumnIndexOrThrow(FIRST_VISIBLE));
|
||||
boolean finished = cursor.getInt(cursor.getColumnIndexOrThrow(FINISHED)) == 1;
|
||||
|
||||
if (Event.hasKey(event)) {
|
||||
records.add(new MegaphoneRecord(Event.fromKey(event), seenCount, lastSeen, firstVisible, finished));
|
||||
} else {
|
||||
Log.w(TAG, "No in-app handing for event '" + event + "'! Deleting it from the database.");
|
||||
missingKeys.add(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String missing : missingKeys) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{missing};
|
||||
|
||||
databaseHelper.getWritableDatabase().delete(TABLE_NAME, query, args);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public void markFirstVisible(@NonNull Event event, long time) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(FIRST_VISIBLE, time);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void markSeen(@NonNull Event event, int seenCount, long lastSeen) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SEEN_COUNT, seenCount);
|
||||
values.put(LAST_SEEN, lastSeen);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void markFinished(@NonNull Event event) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(FINISHED, 1);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
|
||||
}
|
||||
|
||||
public void delete(@NonNull Event event) {
|
||||
String query = EVENT + " = ?";
|
||||
String[] args = new String[]{event.getKey()};
|
||||
|
||||
databaseHelper.getWritableDatabase().delete(TABLE_NAME, query, args);
|
||||
}
|
||||
}
|
||||
@@ -350,9 +350,15 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
}
|
||||
|
||||
private void setReactions(@NonNull SQLiteDatabase db, long messageId, @NonNull ReactionList reactionList) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
ContentValues values = new ContentValues(1);
|
||||
boolean hasReactions = reactionList.getReactionsCount() != 0;
|
||||
|
||||
values.put(REACTIONS, reactionList.getReactionsList().isEmpty() ? null : reactionList.toByteArray());
|
||||
values.put(REACTIONS_UNREAD, reactionList.getReactionsCount() != 0 ? 1 : 0);
|
||||
values.put(REACTIONS_UNREAD, hasReactions ? 1 : 0);
|
||||
|
||||
if (hasReactions) {
|
||||
values.put(NOTIFIED, 0);
|
||||
}
|
||||
|
||||
String query = ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(messageId) };
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.JobDatabase;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
@@ -102,8 +103,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int ATTACHMENT_DISPLAY_ORDER = 42;
|
||||
private static final int SPLIT_PROFILE_NAMES = 43;
|
||||
private static final int STICKER_PACK_ORDER = 44;
|
||||
private static final int MEGAPHONES = 45;
|
||||
private static final int MEGAPHONE_FIRST_APPEARANCE = 46;
|
||||
|
||||
private static final int DATABASE_VERSION = 44;
|
||||
private static final int DATABASE_VERSION = 46;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -146,6 +149,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(StickerDatabase.CREATE_TABLE);
|
||||
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
|
||||
db.execSQL(KeyValueDatabase.CREATE_TABLE);
|
||||
db.execSQL(MegaphoneDatabase.CREATE_TABLE);
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
||||
executeStatements(db, JobDatabase.CREATE_TABLE);
|
||||
|
||||
@@ -708,6 +712,18 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE sticker ADD COLUMN pack_order INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < MEGAPHONES) {
|
||||
db.execSQL("CREATE TABLE megaphone (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"event TEXT UNIQUE, " +
|
||||
"seen_count INTEGER, " +
|
||||
"last_seen INTEGER, " +
|
||||
"finished INTEGER)");
|
||||
}
|
||||
|
||||
if (oldVersion < MEGAPHONE_FIRST_APPEARANCE) {
|
||||
db.execSQL("ALTER TABLE megaphone ADD COLUMN first_visible INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
|
||||
public class MegaphoneRecord {
|
||||
|
||||
private final Megaphones.Event event;
|
||||
private final int seenCount;
|
||||
private final long lastSeen;
|
||||
private final long firstVisible;
|
||||
private final boolean finished;
|
||||
|
||||
public MegaphoneRecord(@NonNull Megaphones.Event event, int seenCount, long lastSeen, long firstVisible, boolean finished) {
|
||||
this.event = event;
|
||||
this.seenCount = seenCount;
|
||||
this.lastSeen = lastSeen;
|
||||
this.firstVisible = firstVisible;
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
public @NonNull Megaphones.Event getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getSeenCount() {
|
||||
return seenCount;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public long getFirstVisible() {
|
||||
return firstVisible;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ReactionRecord {
|
||||
private final String emoji;
|
||||
private final RecipientId author;
|
||||
@@ -36,4 +38,20 @@ public class ReactionRecord {
|
||||
public long getDateReceived() {
|
||||
return dateReceived;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ReactionRecord that = (ReactionRecord) o;
|
||||
return dateSent == that.dateSent &&
|
||||
dateReceived == that.dateReceived &&
|
||||
Objects.equals(emoji, that.emoji) &&
|
||||
Objects.equals(author, that.author);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(emoji, author, dateSent, dateReceived);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.IncomingMessageProcessor;
|
||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
@@ -42,6 +43,7 @@ public class ApplicationDependencies {
|
||||
private static JobManager jobManager;
|
||||
private static FrameRateTracker frameRateTracker;
|
||||
private static KeyValueStore keyValueStore;
|
||||
private static MegaphoneRepository megaphoneRepository;
|
||||
|
||||
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) {
|
||||
@@ -167,6 +169,16 @@ public class ApplicationDependencies {
|
||||
return keyValueStore;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() {
|
||||
assertInitialization();
|
||||
|
||||
if (megaphoneRepository == null) {
|
||||
megaphoneRepository = provider.provideMegaphoneRepository();
|
||||
}
|
||||
|
||||
return megaphoneRepository;
|
||||
}
|
||||
|
||||
private static void assertInitialization() {
|
||||
if (application == null || provider == null) {
|
||||
throw new UninitializedException();
|
||||
@@ -184,6 +196,7 @@ public class ApplicationDependencies {
|
||||
@NonNull JobManager provideJobManager();
|
||||
@NonNull FrameRateTracker provideFrameRateTracker();
|
||||
@NonNull KeyValueStore provideKeyValueStore();
|
||||
@NonNull MegaphoneRepository provideMegaphoneRepository();
|
||||
}
|
||||
|
||||
private static class UninitializedException extends IllegalStateException {
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.push.SecurityEventListener;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
@@ -125,6 +126,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
return new KeyValueStore(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MegaphoneRepository provideMegaphoneRepository() {
|
||||
return new MegaphoneRepository(context);
|
||||
}
|
||||
|
||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
@@ -528,8 +528,9 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
{
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
Recipient sender = Recipient.externalPush(context, content.getSender());
|
||||
Recipient recipient = getMessageDestination(content, message);
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(recipient.getId(),
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
|
||||
message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, true,
|
||||
false,
|
||||
|
||||
@@ -71,8 +71,10 @@ public class SmsReceiveJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public void onRun() throws MigrationPendingException {
|
||||
Log.i(TAG, "onRun()");
|
||||
|
||||
if (TextSecurePreferences.getLocalUuid(context) == null && TextSecurePreferences.getLocalNumber(context) == null) {
|
||||
throw new NotReadyException();
|
||||
}
|
||||
|
||||
Optional<IncomingTextMessage> message = assembleMessageFragments(pdus, subscriptionId);
|
||||
|
||||
if (message.isPresent() && !isBlocked(message.get())) {
|
||||
@@ -167,4 +169,7 @@ public class SmsReceiveJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotReadyException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
@@ -17,6 +19,7 @@ public final class KbsValues {
|
||||
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
|
||||
private static final String TOKEN_RESPONSE = "kbs.token_response";
|
||||
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
|
||||
private static final String KEYBOARD_TYPE = "kbs.keyboard_type";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
@@ -32,6 +35,7 @@ public final class KbsValues {
|
||||
.remove(V2_LOCK_ENABLED)
|
||||
.remove(TOKEN_RESPONSE)
|
||||
.remove(LOCK_LOCAL_PIN_HASH)
|
||||
.remove(KEYBOARD_TYPE)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -112,4 +116,15 @@ public final class KbsValues {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeyboardType(@NonNull PinKeyboardType keyboardType) {
|
||||
store.beginWrite()
|
||||
.putString(KEYBOARD_TYPE, keyboardType.getCode())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public @NonNull PinKeyboardType getKeyboardType() {
|
||||
return PinKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ public class KeyValueDataSet implements KeyValueReader {
|
||||
}
|
||||
}
|
||||
|
||||
boolean containsKey(@NonNull String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Object> getValues() {
|
||||
return values;
|
||||
}
|
||||
@@ -114,10 +118,6 @@ public class KeyValueDataSet implements KeyValueReader {
|
||||
return types.get(key);
|
||||
}
|
||||
|
||||
public boolean containsKey(@NonNull String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
private <E> E readValueAsType(@NonNull String key, Class<E> type, boolean nullable) {
|
||||
Object value = values.get(key);
|
||||
if ((value == null && nullable) || (value != null && value.getClass() == type)) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class RegistrationValues {
|
||||
|
||||
private static final String REGISTRATION_COMPLETE = "registration.complete";
|
||||
private static final String PIN_REQUIRED = "registration.pin_required";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
RegistrationValues(@NonNull KeyValueStore store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public synchronized void onNewInstall() {
|
||||
store.beginWrite()
|
||||
.putBoolean(REGISTRATION_COMPLETE, false)
|
||||
.putBoolean(PIN_REQUIRED, true)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public synchronized void clearRegistrationComplete() {
|
||||
onNewInstall();
|
||||
}
|
||||
|
||||
public synchronized void setRegistrationComplete() {
|
||||
store.beginWrite()
|
||||
.putBoolean(REGISTRATION_COMPLETE, true)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public synchronized boolean pinWasRequiredAtRegistration() {
|
||||
return store.getBoolean(PIN_REQUIRED, false);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public synchronized boolean isRegistrationComplete() {
|
||||
return store.getBoolean(REGISTRATION_COMPLETE, true);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public final class SignalStore {
|
||||
return new KbsValues(getStore());
|
||||
}
|
||||
|
||||
public static RegistrationValues registrationValues() {
|
||||
return new RegistrationValues(getStore());
|
||||
}
|
||||
|
||||
public static String getRemoteConfig() {
|
||||
return getStore().getString(REMOTE_CONFIG, null);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ public class LinkPreviewRepository {
|
||||
public LinkPreviewRepository() {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.proxySelector(new ContentProxySelector())
|
||||
.addInterceptor(new UserAgentInterceptor())
|
||||
.addNetworkInterceptor(new ContentProxySafetyInterceptor())
|
||||
.cache(null)
|
||||
.build();
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.Typeface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
@@ -26,13 +27,20 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.DialogCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -42,8 +50,8 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
@@ -55,10 +63,9 @@ public final class RegistrationLockDialog {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationLockDialog.class);
|
||||
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4;
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4;
|
||||
public static void showReminderIfNecessary(@NonNull Fragment fragment) {
|
||||
final Context context = fragment.requireContext();
|
||||
|
||||
public static void showReminderIfNecessary(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
|
||||
if (!RegistrationLockReminders.needsReminder(context)) return;
|
||||
|
||||
@@ -69,6 +76,86 @@ public final class RegistrationLockDialog {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.pinsForAll()) {
|
||||
showReminder(context, fragment);
|
||||
} else {
|
||||
showLegacyPinReminder(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void showReminder(@NonNull Context context, @NonNull Fragment fragment) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
|
||||
.setView(R.layout.kbs_pin_reminder_view)
|
||||
.setCancelable(false)
|
||||
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
|
||||
.create();
|
||||
|
||||
WindowManager windowManager = ServiceUtil.getWindowManager(context);
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
display.getMetrics(metrics);
|
||||
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
dialog.show();
|
||||
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
|
||||
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
|
||||
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
|
||||
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
|
||||
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
|
||||
|
||||
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
|
||||
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
|
||||
|
||||
pinEditText.requestFocus();
|
||||
|
||||
switch (SignalStore.kbsValues().getKeyboardType()) {
|
||||
case NUMERIC:
|
||||
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
break;
|
||||
case ALPHA_NUMERIC:
|
||||
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
break;
|
||||
}
|
||||
|
||||
ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
|
||||
fragment.startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromForgotPin(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
}
|
||||
};
|
||||
|
||||
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
|
||||
reminder.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
skip.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, false);
|
||||
});
|
||||
|
||||
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper);
|
||||
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
|
||||
? new V2PinVerifier()
|
||||
: new V1PinVerifier(context);
|
||||
|
||||
submit.setOnClickListener(v -> {
|
||||
Editable pinEditable = pinEditText.getText();
|
||||
|
||||
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO [alex]: Remove after pins for all live.
|
||||
*/
|
||||
@Deprecated
|
||||
private static void showLegacyPinReminder(@NonNull Context context) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight)
|
||||
.setView(R.layout.registration_lock_reminder_view)
|
||||
.setCancelable(true)
|
||||
@@ -84,8 +171,8 @@ public final class RegistrationLockDialog {
|
||||
dialog.show();
|
||||
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
EditText pinEditText = dialog.findViewById(R.id.pin);
|
||||
TextView reminder = dialog.findViewById(R.id.reminder);
|
||||
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
|
||||
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
|
||||
|
||||
if (pinEditText == null) throw new AssertionError();
|
||||
if (reminder == null) throw new AssertionError();
|
||||
@@ -126,27 +213,23 @@ public final class RegistrationLockDialog {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
|
||||
if (FeatureFlags.kbs()) {
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
}
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) {
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getPinBackedMasterKey();
|
||||
String localPinHash = kbsValues.getLocalPinHash();
|
||||
|
||||
if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder");
|
||||
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
|
||||
|
||||
return new AfterTextChanged((Editable s) -> {
|
||||
if (s == null) return;
|
||||
String pin = s.toString();
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return;
|
||||
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
|
||||
|
||||
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
|
||||
dialog.dismiss();
|
||||
@@ -178,9 +261,9 @@ public final class RegistrationLockDialog {
|
||||
String pinValue = pin.getText().toString().replace(" ", "");
|
||||
String repeatValue = repeat.getText().toString().replace(" ", "");
|
||||
|
||||
if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) {
|
||||
if (pinValue.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING),
|
||||
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
@@ -201,36 +284,29 @@ public final class RegistrationLockDialog {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
if (FeatureFlags.kbs()) {
|
||||
Log.i(TAG, "Setting pin on KBS");
|
||||
Log.i(TAG, "Setting pin on KBS");
|
||||
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new AssertionError("Failed to set the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new AssertionError("Failed to set the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Setting V1 pin");
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
accountManager.setPin(pinValue);
|
||||
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pinValue);
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
return true;
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) {
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | KeyBackupSystemNoDataException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
@@ -325,4 +401,78 @@ public final class RegistrationLockDialog {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
|
||||
@NonNull AlertDialog dialog,
|
||||
@NonNull TextInputLayout inputWrapper)
|
||||
{
|
||||
return new PinVerifier.Callback() {
|
||||
@Override
|
||||
public void onPinCorrect() {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPinWrong() {
|
||||
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class V1PinVerifier implements PinVerifier {
|
||||
|
||||
private final String pinInPreferences;
|
||||
|
||||
private V1PinVerifier(@NonNull Context context) {
|
||||
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
|
||||
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
|
||||
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
|
||||
callback.onPinCorrect();
|
||||
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
} else {
|
||||
callback.onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class V2PinVerifier implements PinVerifier {
|
||||
|
||||
private final String localPinHash;
|
||||
|
||||
V2PinVerifier() {
|
||||
localPinHash = SignalStore.kbsValues().getLocalPinHash();
|
||||
|
||||
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
|
||||
if (pin == null) return;
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
|
||||
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
|
||||
|
||||
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
|
||||
callback.onPinCorrect();
|
||||
} else {
|
||||
callback.onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface PinVerifier {
|
||||
|
||||
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
|
||||
|
||||
interface Callback {
|
||||
void onPinCorrect();
|
||||
void onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,19 +35,19 @@ public class RegistrationLockReminders {
|
||||
}
|
||||
|
||||
public static void scheduleReminder(@NonNull Context context, boolean success) {
|
||||
Long nextReminderInterval;
|
||||
|
||||
if (success) {
|
||||
long timeSinceLastReminder = System.currentTimeMillis() - TextSecurePreferences.getRegistrationLockLastReminderTime(context);
|
||||
nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
|
||||
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.last();
|
||||
} else {
|
||||
long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
|
||||
nextReminderInterval = INTERVALS.lower(lastReminderInterval);
|
||||
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.first();
|
||||
}
|
||||
Long nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
|
||||
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
|
||||
if (nextReminderInterval == null) {
|
||||
nextReminderInterval = INTERVALS.last();
|
||||
}
|
||||
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
|
||||
} else {
|
||||
long timeSinceLastReminder = TextSecurePreferences.getRegistrationLockLastReminderTime(context) + TimeUnit.MINUTES.toMillis(5);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, timeSinceLastReminder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends Fragment {
|
||||
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private EditText input;
|
||||
private TextView label;
|
||||
private TextView keyboardToggle;
|
||||
private TextView confirm;
|
||||
private LottieAnimationView lottieProgress;
|
||||
private LottieAnimationView lottieEnd;
|
||||
private ViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.base_kbs_pin_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViews(view);
|
||||
|
||||
viewModel = initializeViewModel();
|
||||
viewModel.getUserEntry().observe(getViewLifecycleOwner(), kbsPin -> {
|
||||
boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_NEW_PIN_LENGTH;
|
||||
|
||||
confirm.setEnabled(isEntryValid);
|
||||
confirm.setAlpha(isEntryValid ? 1f : 0.5f);
|
||||
});
|
||||
|
||||
viewModel.getKeyboard().observe(getViewLifecycleOwner(), keyboardType -> {
|
||||
updateKeyboard(keyboardType);
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
initializeListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
protected abstract ViewModel initializeViewModel();
|
||||
|
||||
protected abstract void initializeViewStates();
|
||||
|
||||
protected TextView getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
protected TextView getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
protected EditText getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
protected LottieAnimationView getLottieProgress() {
|
||||
return lottieProgress;
|
||||
}
|
||||
|
||||
protected LottieAnimationView getLottieEnd() {
|
||||
return lottieEnd;
|
||||
}
|
||||
|
||||
protected TextView getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
protected TextView getKeyboardToggle() {
|
||||
return keyboardToggle;
|
||||
}
|
||||
|
||||
protected TextView getConfirm() {
|
||||
return confirm;
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
title = view.findViewById(R.id.edit_kbs_pin_title);
|
||||
description = view.findViewById(R.id.edit_kbs_pin_description);
|
||||
input = view.findViewById(R.id.edit_kbs_pin_input);
|
||||
label = view.findViewById(R.id.edit_kbs_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.edit_kbs_pin_keyboard_toggle);
|
||||
confirm = view.findViewById(R.id.edit_kbs_pin_confirm);
|
||||
lottieProgress = view.findViewById(R.id.edit_kbs_pin_lottie_progress);
|
||||
lottieEnd = view.findViewById(R.id.edit_kbs_pin_lottie_end);
|
||||
|
||||
initializeViewStates();
|
||||
}
|
||||
|
||||
private void initializeListeners() {
|
||||
input.addTextChangedListener(new AfterTextChanged(s -> viewModel.setUserEntry(s.toString())));
|
||||
input.setImeOptions(EditorInfo.IME_ACTION_NEXT);
|
||||
input.setOnEditorActionListener(this::handleEditorAction);
|
||||
keyboardToggle.setOnClickListener(v -> viewModel.toggleAlphaNumeric());
|
||||
confirm.setOnClickListener(v -> viewModel.confirm());
|
||||
}
|
||||
|
||||
private boolean handleEditorAction(@NonNull View view, int actionId, @NonNull KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT && confirm.isEnabled()) {
|
||||
viewModel.confirm();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
input.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
private @StringRes int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.BaseKbsPinFragment__create_numeric_pin;
|
||||
} else {
|
||||
return R.string.BaseKbsPinFragment__create_alphanumeric_pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
interface BaseKbsPinViewModel {
|
||||
LiveData<KbsPin> getUserEntry();
|
||||
|
||||
LiveData<PinKeyboardType> getKeyboard();
|
||||
|
||||
@MainThread
|
||||
void setUserEntry(String userEntry);
|
||||
|
||||
@MainThread
|
||||
void toggleAlphaNumeric();
|
||||
|
||||
@MainThread
|
||||
void confirm();
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RawRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.animation.AnimationRepeatListener;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewModel> {
|
||||
|
||||
private ConfirmKbsPinViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void initializeViewStates() {
|
||||
ConfirmKbsPinFragmentArgs args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
if (args.getIsPinChange()) {
|
||||
initializeViewStatesForPinChange();
|
||||
} else {
|
||||
initializeViewStatesForPinCreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfirmKbsPinViewModel initializeViewModel() {
|
||||
ConfirmKbsPinFragmentArgs args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
KbsPin userEntry = Preconditions.checkNotNull(args.getUserEntry());
|
||||
PinKeyboardType keyboard = args.getKeyboard();
|
||||
ConfirmKbsPinRepository repository = new ConfirmKbsPinRepository();
|
||||
ConfirmKbsPinViewModel.Factory factory = new ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ConfirmKbsPinViewModel.class);
|
||||
|
||||
viewModel.getLabel().observe(getViewLifecycleOwner(), this::updateLabel);
|
||||
viewModel.getSaveAnimation().observe(getViewLifecycleOwner(), this::updateSaveAnimation);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPinCreate() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
|
||||
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
|
||||
getKeyboardToggle().setVisibility(View.INVISIBLE);
|
||||
getLabel().setText("");
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPinChange() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
|
||||
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
|
||||
getKeyboardToggle().setVisibility(View.INVISIBLE);
|
||||
getLabel().setText("");
|
||||
}
|
||||
|
||||
private void updateLabel(@NonNull ConfirmKbsPinViewModel.Label label) {
|
||||
switch (label) {
|
||||
case EMPTY:
|
||||
getLabel().setText("");
|
||||
break;
|
||||
case CREATING_PIN:
|
||||
getLabel().setText(R.string.ConfirmKbsPinFragment__creating_pin);
|
||||
break;
|
||||
case RE_ENTER_PIN:
|
||||
getLabel().setText(R.string.ConfirmKbsPinFragment__re_enter_pin);
|
||||
break;
|
||||
case PIN_DOES_NOT_MATCH:
|
||||
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red),
|
||||
getString(R.string.ConfirmKbsPinFragment__pins_dont_match)));
|
||||
getInput().getText().clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSaveAnimation(@NonNull ConfirmKbsPinViewModel.SaveAnimation animation) {
|
||||
updateAnimationAndInputVisibility(animation);
|
||||
LottieAnimationView lottieProgress = getLottieProgress();
|
||||
|
||||
switch (animation) {
|
||||
case NONE:
|
||||
lottieProgress.cancelAnimation();
|
||||
break;
|
||||
case LOADING:
|
||||
lottieProgress.setAnimation(R.raw.lottie_kbs_loading);
|
||||
lottieProgress.setRepeatMode(LottieDrawable.RESTART);
|
||||
lottieProgress.setRepeatCount(LottieDrawable.INFINITE);
|
||||
lottieProgress.playAnimation();
|
||||
break;
|
||||
case SUCCESS:
|
||||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_success, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
requireActivity().setResult(Activity.RESULT_OK);
|
||||
closeNavGraphBranch();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case FAILURE:
|
||||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
displayFailedDialog();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void startEndAnimationOnNextProgressRepetition(@RawRes int lottieAnimationId,
|
||||
@NonNull AnimationCompleteListener listener)
|
||||
{
|
||||
LottieAnimationView lottieProgress = getLottieProgress();
|
||||
LottieAnimationView lottieEnd = getLottieEnd();
|
||||
|
||||
lottieEnd.setAnimation(lottieAnimationId);
|
||||
lottieEnd.removeAllAnimatorListeners();
|
||||
lottieEnd.setRepeatCount(0);
|
||||
lottieEnd.addAnimatorListener(listener);
|
||||
|
||||
if (lottieProgress.isAnimating()) {
|
||||
lottieProgress.addAnimatorListener(new AnimationRepeatListener(animator ->
|
||||
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd)
|
||||
));
|
||||
} else {
|
||||
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideProgressAndStartEndAnimation(@NonNull LottieAnimationView lottieProgress,
|
||||
@NonNull LottieAnimationView lottieEnd)
|
||||
{
|
||||
viewModel.onLoadingAnimationComplete();
|
||||
lottieProgress.setVisibility(View.GONE);
|
||||
lottieEnd.setVisibility(View.VISIBLE);
|
||||
lottieEnd.playAnimation();
|
||||
}
|
||||
|
||||
private void updateAnimationAndInputVisibility(ConfirmKbsPinViewModel.SaveAnimation saveAnimation) {
|
||||
if (saveAnimation == ConfirmKbsPinViewModel.SaveAnimation.NONE) {
|
||||
getInput().setVisibility(View.VISIBLE);
|
||||
getLottieProgress().setVisibility(View.GONE);
|
||||
} else {
|
||||
getInput().setVisibility(View.GONE);
|
||||
getLottieProgress().setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayFailedDialog() {
|
||||
new AlertDialog.Builder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed)
|
||||
.setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, (d, w) -> {
|
||||
d.dismiss();
|
||||
markMegaphoneSeenIfNecessary();
|
||||
requireActivity().setResult(Activity.RESULT_CANCELED);
|
||||
closeNavGraphBranch();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void closeNavGraphBranch() {
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
if (activityIntent != null && activityIntent.hasExtra("next_intent")) {
|
||||
startActivity(activityIntent.getParcelableExtra("next_intent"));
|
||||
}
|
||||
|
||||
requireActivity().finish();
|
||||
}
|
||||
|
||||
private void markMegaphoneSeenIfNecessary() {
|
||||
ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
final class ConfirmKbsPinRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ConfirmKbsPinRepository.class);
|
||||
|
||||
void setPin(@NonNull KbsPin kbsPin, @NonNull PinKeyboardType keyboard, @NonNull Consumer<PinSetResult> resultConsumer) {
|
||||
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
String pinValue = kbsPin.toString();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
Log.i(TAG, "Setting pin on KBS");
|
||||
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new AssertionError("Failed to set the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
SignalStore.kbsValues().setKeyboardType(keyboard);
|
||||
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
|
||||
|
||||
return PinSetResult.SUCCESS;
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException | KeyBackupSystemNoDataException e) {
|
||||
Log.w(TAG, e);
|
||||
return PinSetResult.FAILURE;
|
||||
}
|
||||
}, resultConsumer::accept);
|
||||
}
|
||||
|
||||
enum PinSetResult {
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinRepository.PinSetResult;
|
||||
|
||||
final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
||||
|
||||
private final ConfirmKbsPinRepository repository;
|
||||
|
||||
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
||||
private final MutableLiveData<PinKeyboardType> keyboard = new MutableLiveData<>(PinKeyboardType.NUMERIC);
|
||||
private final MutableLiveData<SaveAnimation> saveAnimation = new MutableLiveData<>(SaveAnimation.NONE);
|
||||
private final MutableLiveData<Label> label = new MutableLiveData<>(Label.RE_ENTER_PIN);
|
||||
|
||||
private final KbsPin pinToConfirm;
|
||||
|
||||
private ConfirmKbsPinViewModel(@NonNull KbsPin pinToConfirm,
|
||||
@NonNull PinKeyboardType keyboard,
|
||||
@NonNull ConfirmKbsPinRepository repository)
|
||||
{
|
||||
this.keyboard.setValue(keyboard);
|
||||
|
||||
this.pinToConfirm = pinToConfirm;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
LiveData<SaveAnimation> getSaveAnimation() {
|
||||
return Transformations.distinctUntilChanged(saveAnimation);
|
||||
}
|
||||
|
||||
LiveData<Label> getLabel() {
|
||||
return Transformations.distinctUntilChanged(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirm() {
|
||||
KbsPin userEntry = this.userEntry.getValue();
|
||||
this.userEntry.setValue(KbsPin.EMPTY);
|
||||
|
||||
if (pinToConfirm.toString().equals(userEntry.toString())) {
|
||||
this.label.setValue(Label.CREATING_PIN);
|
||||
this.saveAnimation.setValue(SaveAnimation.LOADING);
|
||||
|
||||
repository.setPin(pinToConfirm, Preconditions.checkNotNull(this.keyboard.getValue()), this::handleResult);
|
||||
} else {
|
||||
this.label.setValue(Label.PIN_DOES_NOT_MATCH);
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadingAnimationComplete() {
|
||||
this.label.setValue(Label.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<KbsPin> getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<PinKeyboardType> getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setUserEntry(String userEntry) {
|
||||
this.userEntry.setValue(KbsPin.from(userEntry));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void toggleAlphaNumeric() {
|
||||
this.keyboard.setValue(this.keyboard.getValue().getOther());
|
||||
}
|
||||
|
||||
private void handleResult(PinSetResult result) {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
this.saveAnimation.setValue(SaveAnimation.SUCCESS);
|
||||
break;
|
||||
case FAILURE:
|
||||
this.saveAnimation.setValue(SaveAnimation.FAILURE);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown state: " + result.name());
|
||||
}
|
||||
}
|
||||
|
||||
enum Label {
|
||||
RE_ENTER_PIN,
|
||||
PIN_DOES_NOT_MATCH,
|
||||
CREATING_PIN,
|
||||
EMPTY
|
||||
}
|
||||
|
||||
enum SaveAnimation {
|
||||
NONE,
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final KbsPin pinToConfirm;
|
||||
private final PinKeyboardType keyboard;
|
||||
private final ConfirmKbsPinRepository repository;
|
||||
|
||||
Factory(@NonNull KbsPin pinToConfirm,
|
||||
@NonNull PinKeyboardType keyboard,
|
||||
@NonNull ConfirmKbsPinRepository repository)
|
||||
{
|
||||
this.pinToConfirm = pinToConfirm;
|
||||
this.keyboard = keyboard;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new ConfirmKbsPinViewModel(pinToConfirm, keyboard, repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class CreateKbsPinActivity extends BaseActionBarActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = 27698;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
public static @NonNull Intent getIntentForPinCreate(@NonNull Context context) {
|
||||
CreateKbsPinFragmentArgs args = new CreateKbsPinFragmentArgs.Builder()
|
||||
.setIsForgotPin(false)
|
||||
.setIsPinChange(false)
|
||||
.build();
|
||||
|
||||
return getIntentWithArgs(context, args);
|
||||
}
|
||||
|
||||
public static @NonNull Intent getIntentForPinChangeFromForgotPin(@NonNull Context context) {
|
||||
CreateKbsPinFragmentArgs args = new CreateKbsPinFragmentArgs.Builder()
|
||||
.setIsForgotPin(true)
|
||||
.setIsPinChange(true)
|
||||
.build();
|
||||
|
||||
return getIntentWithArgs(context, args);
|
||||
}
|
||||
|
||||
public static @NonNull Intent getIntentForPinChangeFromSettings(@NonNull Context context) {
|
||||
CreateKbsPinFragmentArgs args = new CreateKbsPinFragmentArgs.Builder()
|
||||
.setIsForgotPin(false)
|
||||
.setIsPinChange(true)
|
||||
.build();
|
||||
|
||||
return getIntentWithArgs(context, args);
|
||||
}
|
||||
|
||||
private static @NonNull Intent getIntentWithArgs(@NonNull Context context, @NonNull CreateKbsPinFragmentArgs args) {
|
||||
return new Intent(context, CreateKbsPinActivity.class).putExtras(args.toBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
startActivity(getPromptPassphraseIntent());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.create_kbs_pin_activity);
|
||||
|
||||
CreateKbsPinFragmentArgs arguments = CreateKbsPinFragmentArgs.fromBundle(getIntent().getExtras());
|
||||
|
||||
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
|
||||
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
|
||||
|
||||
private static final int PIN_LOCKOUT_DAYS = 7;
|
||||
|
||||
@Override
|
||||
protected void initializeViewStates() {
|
||||
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
if (args.getIsPinChange()) {
|
||||
initializeViewStatesForPinChange(args.getIsForgotPin());
|
||||
} else {
|
||||
initializeViewStatesForPinCreate();
|
||||
}
|
||||
|
||||
getLabel().setText(getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits));
|
||||
getConfirm().setEnabled(false);
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPinChange(boolean isForgotPin) {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
|
||||
|
||||
if (isForgotPin) {
|
||||
getDescription().setText(requireContext().getResources()
|
||||
.getQuantityString(R.plurals.CreateKbsPinFragment__you_can_choose_a_new_pin_because_this_device_is_registered,
|
||||
PIN_LOCKOUT_DAYS,
|
||||
PIN_LOCKOUT_DAYS));
|
||||
} else {
|
||||
getDescription().setText(R.string.CreateKbsPinFragment__pins_add_an_extra_layer_of_security);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPinCreate() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
|
||||
getDescription().setText(R.string.CreateKbsPinFragment__pins_add_an_extra_layer_of_security);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CreateKbsPinViewModel initializeViewModel() {
|
||||
CreateKbsPinViewModel viewModel = ViewModelProviders.of(this).get(CreateKbsPinViewModel.class);
|
||||
CreateKbsPinFragmentArgs args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
|
||||
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard(), args.getIsPinChange()));
|
||||
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> {
|
||||
getLabel().setText(getLabelText(k));
|
||||
getInput().getText().clear();
|
||||
});
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void onConfirmPin(@NonNull KbsPin userEntry, @NonNull PinKeyboardType keyboard, boolean isPinChange) {
|
||||
CreateKbsPinFragmentDirections.ActionConfirmPin action = CreateKbsPinFragmentDirections.actionConfirmPin();
|
||||
|
||||
action.setUserEntry(userEntry);
|
||||
action.setKeyboard(keyboard);
|
||||
action.setIsPinChange(isPinChange);
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(action);
|
||||
}
|
||||
|
||||
private String getLabelText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_characters);
|
||||
} else {
|
||||
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
|
||||
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_NEW_PIN_LENGTH, KbsConstants.MINIMUM_NEW_PIN_LENGTH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
||||
|
||||
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
||||
private final MutableLiveData<PinKeyboardType> keyboard = new MutableLiveData<>(PinKeyboardType.NUMERIC);
|
||||
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
|
||||
|
||||
@Override
|
||||
public LiveData<KbsPin> getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<PinKeyboardType> getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
LiveData<NavigationEvent> getNavigationEvents() { return events; }
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void setUserEntry(String userEntry) {
|
||||
this.userEntry.setValue(KbsPin.from(userEntry));
|
||||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void toggleAlphaNumeric() {
|
||||
this.keyboard.setValue(Preconditions.checkNotNull(this.keyboard.getValue()).getOther());
|
||||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void confirm() {
|
||||
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
|
||||
Preconditions.checkNotNull(this.getKeyboard().getValue())));
|
||||
}
|
||||
|
||||
static final class NavigationEvent {
|
||||
private final KbsPin userEntry;
|
||||
private final PinKeyboardType keyboard;
|
||||
|
||||
NavigationEvent(@NonNull KbsPin userEntry, @NonNull PinKeyboardType keyboard) {
|
||||
this.userEntry = userEntry;
|
||||
this.keyboard = keyboard;
|
||||
}
|
||||
|
||||
KbsPin getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
PinKeyboardType getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
public final class KbsConstants {
|
||||
|
||||
static final int MINIMUM_NEW_PIN_LENGTH = 6;
|
||||
|
||||
/** Migrated pins from V1 might be 4 */
|
||||
public static final int MINIMUM_POSSIBLE_PIN_LENGTH = 4;
|
||||
|
||||
private KbsConstants() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class KbsMigrationActivity extends BaseActionBarActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = CreateKbsPinActivity.REQUEST_NEW_PIN;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
public static Intent createIntent() {
|
||||
return new Intent(ApplicationDependencies.getApplication(), KbsMigrationActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
startActivity(getPromptPassphraseIntent());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.kbs_migration_activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class KbsPin implements Parcelable {
|
||||
|
||||
public static KbsPin EMPTY = new KbsPin("");
|
||||
|
||||
private final String pin;
|
||||
|
||||
private KbsPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
private KbsPin(Parcel in) {
|
||||
pin = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public static KbsPin from(@Nullable String pin) {
|
||||
if (pin == null) return EMPTY;
|
||||
|
||||
pin = pin.trim();
|
||||
|
||||
if (pin.length() == 0) return EMPTY;
|
||||
|
||||
return new KbsPin(pin);
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return pin.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(pin);
|
||||
}
|
||||
|
||||
public static final Creator<KbsPin> CREATOR = new Creator<KbsPin>() {
|
||||
@Override
|
||||
public KbsPin createFromParcel(Parcel in) {
|
||||
return new KbsPin(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KbsPin[] newArray(int size) {
|
||||
return new KbsPin[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class KbsSplashFragment extends Fragment {
|
||||
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private TextView primaryAction;
|
||||
private TextView secondaryAction;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.kbs_splash_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
title = view.findViewById(R.id.kbs_splash_title);
|
||||
description = view.findViewById(R.id.kbs_splash_description);
|
||||
primaryAction = view.findViewById(R.id.kbs_splash_primary_action);
|
||||
secondaryAction = view.findViewById(R.id.kbs_splash_secondary_action);
|
||||
|
||||
primaryAction.setOnClickListener(v -> onCreatePin());
|
||||
secondaryAction.setOnClickListener(v -> onLearnMore());
|
||||
|
||||
if (PinUtil.userHasPin(requireContext())) {
|
||||
setUpRegLockEnabled();
|
||||
} else {
|
||||
setUpRegLockDisabled();
|
||||
}
|
||||
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() { }
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpRegLockEnabled() {
|
||||
title.setText(R.string.KbsSplashFragment__registration_lock_equals_pin);
|
||||
description.setText(R.string.KbsSplashFragment__your_registration_lock_is_now_called_a_pin);
|
||||
primaryAction.setText(R.string.KbsSplashFragment__update_pin);
|
||||
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
|
||||
}
|
||||
|
||||
private void setUpRegLockDisabled() {
|
||||
title.setText(R.string.KbsSplashFragment__introducing_pins);
|
||||
description.setText(R.string.KbsSplashFragment__pins_add_another_level_of_security_to_your_account);
|
||||
primaryAction.setText(R.string.KbsSplashFragment__create_your_pin);
|
||||
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
|
||||
}
|
||||
|
||||
private void onCreatePin() {
|
||||
KbsSplashFragmentDirections.ActionCreateKbsPin action = KbsSplashFragmentDirections.actionCreateKbsPin();
|
||||
|
||||
action.setIsPinChange(PinUtil.userHasPin(requireContext()));
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(action);
|
||||
}
|
||||
|
||||
private void onLearnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setData(Uri.parse(getString(R.string.KbsSplashFragment__learn_more_link)));
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum PinKeyboardType {
|
||||
NUMERIC("numeric"),
|
||||
ALPHA_NUMERIC("alphaNumeric");
|
||||
|
||||
private final String code;
|
||||
|
||||
PinKeyboardType(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public PinKeyboardType getOther() {
|
||||
if (this == NUMERIC) return ALPHA_NUMERIC;
|
||||
else return NUMERIC;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static PinKeyboardType fromCode(@Nullable String code) {
|
||||
for (PinKeyboardType type : PinKeyboardType.values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return NUMERIC;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public final class PinUtil {
|
||||
|
||||
private PinUtil() {}
|
||||
|
||||
public static boolean userHasPin(@NonNull Context context) {
|
||||
return TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,7 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
mediaRecord.getAttachment().getWidth(),
|
||||
mediaRecord.getAttachment().getHeight(),
|
||||
mediaRecord.getAttachment().getSize(),
|
||||
0,
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(mediaRecord.getAttachment().getCaption()));
|
||||
}
|
||||
|
||||
@@ -75,6 +75,12 @@ public final class LegacyCameraModels {
|
||||
add("COL-AL10");
|
||||
add("COL-L29");
|
||||
add("COL-L19");
|
||||
|
||||
// Samsung Galaxy S6
|
||||
add("SM-G920F");
|
||||
|
||||
// Honor View 10
|
||||
add("BLK-L09");
|
||||
}};
|
||||
|
||||
private LegacyCameraModels() {
|
||||
|
||||
@@ -20,17 +20,28 @@ public class Media implements Parcelable {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final long size;
|
||||
private final long duration;
|
||||
|
||||
private Optional<String> bucketId;
|
||||
private Optional<String> caption;
|
||||
|
||||
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional<String> bucketId, Optional<String> caption) {
|
||||
public Media(@NonNull Uri uri,
|
||||
@NonNull String mimeType,
|
||||
long date,
|
||||
int width,
|
||||
int height,
|
||||
long size,
|
||||
long duration,
|
||||
Optional<String> bucketId,
|
||||
Optional<String> caption)
|
||||
{
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.date = date;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.size = size;
|
||||
this.duration = duration;
|
||||
this.bucketId = bucketId;
|
||||
this.caption = caption;
|
||||
}
|
||||
@@ -42,6 +53,7 @@ public class Media implements Parcelable {
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
size = in.readLong();
|
||||
duration = in.readLong();
|
||||
bucketId = Optional.fromNullable(in.readString());
|
||||
caption = Optional.fromNullable(in.readString());
|
||||
}
|
||||
@@ -70,6 +82,10 @@ public class Media implements Parcelable {
|
||||
return size;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public Optional<String> getBucketId() {
|
||||
return bucketId;
|
||||
}
|
||||
@@ -95,6 +111,7 @@ public class Media implements Parcelable {
|
||||
dest.writeInt(width);
|
||||
dest.writeInt(height);
|
||||
dest.writeLong(size);
|
||||
dest.writeLong(duration);
|
||||
dest.writeString(bucketId.orNull());
|
||||
dest.writeString(caption.orNull());
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.util.Map;
|
||||
/**
|
||||
* Handles the retrieval of media present on the user's device.
|
||||
*/
|
||||
class MediaRepository {
|
||||
public class MediaRepository {
|
||||
|
||||
private static final String TAG = Log.tag(MediaRepository.class);
|
||||
|
||||
@@ -56,7 +56,7 @@ class MediaRepository {
|
||||
/**
|
||||
* Retrieves a list of media items (images and videos) that are present int he specified bucket.
|
||||
*/
|
||||
void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback<List<Media>> callback) {
|
||||
public void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback<List<Media>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
|
||||
}
|
||||
|
||||
@@ -141,9 +141,9 @@ class MediaRepository {
|
||||
long thumbnailTimestamp = 0;
|
||||
Map<String, FolderData> folders = new HashMap<>();
|
||||
|
||||
String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_TAKEN };
|
||||
String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED };
|
||||
String selection = Images.Media.DATA + " NOT NULL";
|
||||
String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_TAKEN + " DESC";
|
||||
String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_MODIFIED + " DESC";
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, null, sortBy)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
@@ -189,18 +189,18 @@ class MediaRepository {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) {
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
|
||||
List<Media> media = new LinkedList<>();
|
||||
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
||||
String[] selectionArgs = new String[] { bucketId };
|
||||
String sortBy = Images.Media.DATE_TAKEN + " DESC";
|
||||
String sortBy = Images.Media.DATE_MODIFIED + " DESC";
|
||||
|
||||
String[] projection;
|
||||
|
||||
if (hasOrientation) {
|
||||
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
if (isImage) {
|
||||
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
} else {
|
||||
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE, Video.Media.DURATION};
|
||||
}
|
||||
|
||||
if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) {
|
||||
@@ -213,13 +213,14 @@ class MediaRepository {
|
||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
|
||||
Uri uri = Uri.fromFile(new File(path));
|
||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN));
|
||||
int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED));
|
||||
int orientation = isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
|
||||
long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0;
|
||||
|
||||
media.add(new Media(uri, mimetype, dateTaken, width, height, size, Optional.of(bucketId), Optional.absent()));
|
||||
media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +269,7 @@ class MediaRepository {
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.put(media, updated);
|
||||
} catch (IOException e) {
|
||||
@@ -332,7 +333,7 @@ class MediaRepository {
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
|
||||
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
|
||||
}
|
||||
|
||||
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
|
||||
@@ -358,7 +359,7 @@ class MediaRepository {
|
||||
height = dimens.second;
|
||||
}
|
||||
|
||||
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption());
|
||||
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption());
|
||||
}
|
||||
|
||||
private static class FolderResult {
|
||||
@@ -433,7 +434,7 @@ class MediaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
public interface Callback<E> {
|
||||
void onComplete(@NonNull E result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +412,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
width,
|
||||
height,
|
||||
length,
|
||||
0,
|
||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
||||
Optional.absent()
|
||||
);
|
||||
|
||||
@@ -303,10 +303,11 @@ class MediaSendViewModel extends ViewModel {
|
||||
captionVisible = false;
|
||||
|
||||
List<Media> uncaptioned = Stream.of(getSelectedMediaOrDefault())
|
||||
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getBucketId(), Optional.absent()))
|
||||
.map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent()))
|
||||
.toList();
|
||||
|
||||
selectedMedia.setValue(uncaptioned);
|
||||
position.setValue(position.getValue() != null ? position.getValue() : 0);
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
@@ -476,7 +477,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
|
||||
if (splitMessage.getTextSlide().isPresent()) {
|
||||
Slide slide = splitMessage.getTextSlide().get();
|
||||
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), Optional.absent(), Optional.absent()), recipient);
|
||||
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent()), recipient);
|
||||
}
|
||||
|
||||
uploadRepository.applyMediaUpdates(oldToNew, recipient);
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class BasicMegaphoneView extends FrameLayout {
|
||||
|
||||
private ImageView image;
|
||||
private TextView titleText;
|
||||
private TextView bodyText;
|
||||
private Button actionButton;
|
||||
private Button snoozeButton;
|
||||
|
||||
private Megaphone megaphone;
|
||||
private MegaphoneListener megaphoneListener;
|
||||
|
||||
public BasicMegaphoneView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public BasicMegaphoneView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(@NonNull Context context) {
|
||||
inflate(context, R.layout.basic_megaphone_view, this);
|
||||
|
||||
this.image = findViewById(R.id.basic_megaphone_image);
|
||||
this.titleText = findViewById(R.id.basic_megaphone_title);
|
||||
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
||||
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
||||
this.snoozeButton = findViewById(R.id.basic_megaphone_snooze);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener megaphoneListener) {
|
||||
this.megaphone = megaphone;
|
||||
this.megaphoneListener = megaphoneListener;
|
||||
|
||||
if (megaphone.getImage() != 0) {
|
||||
image.setVisibility(VISIBLE);
|
||||
image.setImageResource(megaphone.getImage());
|
||||
} else {
|
||||
image.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getTitle() != 0) {
|
||||
titleText.setVisibility(VISIBLE);
|
||||
titleText.setText(megaphone.getTitle());
|
||||
} else {
|
||||
titleText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getBody() != 0) {
|
||||
bodyText.setVisibility(VISIBLE);
|
||||
bodyText.setText(megaphone.getBody());
|
||||
} else {
|
||||
bodyText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.getButtonText() != 0) {
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setText(megaphone.getButtonText());
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (megaphone.getButtonClickListener() != null) {
|
||||
megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.canSnooze()) {
|
||||
snoozeButton.setVisibility(VISIBLE);
|
||||
snoozeButton.setOnClickListener(v -> {
|
||||
megaphoneListener.onMegaphoneSnooze(megaphone);
|
||||
|
||||
if (megaphone.getSnoozeListener() != null) {
|
||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
final class ForeverSchedule implements MegaphoneSchedule {
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
ForeverSchedule(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
||||
|
||||
/**
|
||||
* For guidance on creating megaphones, see {@link Megaphones}.
|
||||
*/
|
||||
public class Megaphone {
|
||||
|
||||
private final Event event;
|
||||
private final Style style;
|
||||
private final boolean mandatory;
|
||||
private final boolean canSnooze;
|
||||
private final int titleRes;
|
||||
private final int bodyRes;
|
||||
private final int imageRes;
|
||||
private final int buttonTextRes;
|
||||
private final EventListener buttonListener;
|
||||
private final EventListener snoozeListener;
|
||||
private final EventListener onVisibleListener;
|
||||
|
||||
private Megaphone(@NonNull Builder builder) {
|
||||
this.event = builder.event;
|
||||
this.style = builder.style;
|
||||
this.mandatory = builder.mandatory;
|
||||
this.canSnooze = builder.canSnooze;
|
||||
this.titleRes = builder.titleRes;
|
||||
this.bodyRes = builder.bodyRes;
|
||||
this.imageRes = builder.imageRes;
|
||||
this.buttonTextRes = builder.buttonTextRes;
|
||||
this.buttonListener = builder.buttonListener;
|
||||
this.snoozeListener = builder.snoozeListener;
|
||||
this.onVisibleListener = builder.onVisibleListener;
|
||||
}
|
||||
|
||||
public @NonNull Event getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public boolean isMandatory() {
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
public boolean canSnooze() {
|
||||
return canSnooze;
|
||||
}
|
||||
|
||||
public @NonNull Style getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
public @StringRes int getTitle() {
|
||||
return titleRes;
|
||||
}
|
||||
|
||||
public @StringRes int getBody() {
|
||||
return bodyRes;
|
||||
}
|
||||
|
||||
public @DrawableRes int getImage() {
|
||||
return imageRes;
|
||||
}
|
||||
|
||||
public @StringRes int getButtonText() {
|
||||
return buttonTextRes;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getButtonClickListener() {
|
||||
return buttonListener;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getSnoozeListener() {
|
||||
return snoozeListener;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getOnVisibleListener() {
|
||||
return onVisibleListener;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final Event event;
|
||||
private final Style style;
|
||||
|
||||
private boolean mandatory;
|
||||
private boolean canSnooze;
|
||||
private int titleRes;
|
||||
private int bodyRes;
|
||||
private int imageRes;
|
||||
private int buttonTextRes;
|
||||
private EventListener buttonListener;
|
||||
private EventListener snoozeListener;
|
||||
private EventListener onVisibleListener;
|
||||
|
||||
|
||||
public Builder(@NonNull Event event, @NonNull Style style) {
|
||||
this.event = event;
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
public @NonNull Builder setMandatory(boolean mandatory) {
|
||||
this.mandatory = mandatory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder enableSnooze(@Nullable EventListener listener) {
|
||||
this.canSnooze = true;
|
||||
this.snoozeListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder disableSnooze() {
|
||||
this.canSnooze = false;
|
||||
this.snoozeListener = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setTitle(@StringRes int titleRes) {
|
||||
this.titleRes = titleRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setBody(@StringRes int bodyRes) {
|
||||
this.bodyRes = bodyRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setImage(@DrawableRes int imageRes) {
|
||||
this.imageRes = imageRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull EventListener listener) {
|
||||
this.buttonTextRes = buttonTextRes;
|
||||
this.buttonListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
|
||||
this.onVisibleListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Megaphone build() {
|
||||
return new Megaphone(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum Style {
|
||||
REACTIONS, BASIC, FULLSCREEN
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public interface MegaphoneListener {
|
||||
/**
|
||||
* When a megaphone wants to navigate to a specific intent.
|
||||
*/
|
||||
void onMegaphoneNavigationRequested(@NonNull Intent intent);
|
||||
|
||||
/**
|
||||
* When a megaphone wants to navigate to a specific intent for a request code.
|
||||
*/
|
||||
void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode);
|
||||
|
||||
/**
|
||||
* When a megaphone wants to show a toast/snackbar.
|
||||
*/
|
||||
void onMegaphoneToastRequested(@NonNull String string);
|
||||
|
||||
/**
|
||||
* When a megaphone has been snoozed via "remind me later" or a similar option.
|
||||
*/
|
||||
void onMegaphoneSnooze(@NonNull Megaphone megaphone);
|
||||
|
||||
/**
|
||||
* Called when a megaphone completed its goal.
|
||||
*/
|
||||
void onMegaphoneCompleted(@NonNull Megaphone megaphone);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Synchronization of data structures is done using a serial executor. Do not access or change
|
||||
* data structures or fields on anything except the executor.
|
||||
*/
|
||||
public class MegaphoneRepository {
|
||||
|
||||
private final Context context;
|
||||
private final Executor executor;
|
||||
private final MegaphoneDatabase database;
|
||||
private final Map<Event, MegaphoneRecord> databaseCache;
|
||||
|
||||
private boolean enabled;
|
||||
|
||||
public MegaphoneRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.executor = SignalExecutors.SERIAL;
|
||||
this.database = DatabaseFactory.getMegaphoneDatabase(context);
|
||||
this.databaseCache = new HashMap<>();
|
||||
|
||||
executor.execute(this::init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks any megaphones a new user shouldn't see as "finished".
|
||||
*/
|
||||
@MainThread
|
||||
public void onFirstEverAppLaunch() {
|
||||
executor.execute(() -> {
|
||||
database.markFinished(Event.REACTIONS);
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onAppForegrounded() {
|
||||
executor.execute(() -> enabled = true);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void getNextMegaphone(@NonNull Callback<Megaphone> callback) {
|
||||
executor.execute(() -> {
|
||||
if (enabled) {
|
||||
callback.onResult(Megaphones.getNextMegaphone(context, databaseCache));
|
||||
} else {
|
||||
callback.onResult(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void markVisible(@NonNull Megaphones.Event event) {
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
executor.execute(() -> {
|
||||
if (getRecord(event).getFirstVisible() == 0) {
|
||||
database.markFirstVisible(event, time);
|
||||
resetDatabaseCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void markSeen(@NonNull Event event) {
|
||||
long lastSeen = System.currentTimeMillis();
|
||||
|
||||
executor.execute(() -> {
|
||||
MegaphoneRecord record = getRecord(event);
|
||||
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
|
||||
enabled = false;
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void markFinished(@NonNull Event event) {
|
||||
executor.execute(() -> {
|
||||
database.markFinished(event);
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void init() {
|
||||
List<MegaphoneRecord> records = database.getAllAndDeleteMissing();
|
||||
Set<Event> events = Stream.of(records).map(MegaphoneRecord::getEvent).collect(Collectors.toSet());
|
||||
Set<Event> missing = Stream.of(Megaphones.Event.values()).filterNot(events::contains).collect(Collectors.toSet());
|
||||
|
||||
database.insert(missing);
|
||||
resetDatabaseCache();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull MegaphoneRecord getRecord(@NonNull Event event) {
|
||||
//noinspection ConstantConditions
|
||||
return databaseCache.get(event);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void resetDatabaseCache() {
|
||||
databaseCache.clear();
|
||||
databaseCache.putAll(Stream.of(database.getAllAndDeleteMissing()).collect(Collectors.toMap(MegaphoneRecord::getEvent, m -> m)));
|
||||
}
|
||||
|
||||
public interface Callback<E> {
|
||||
void onResult(E result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
public interface MegaphoneSchedule {
|
||||
boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsMegaphoneView;
|
||||
|
||||
public class MegaphoneViewBuilder {
|
||||
|
||||
public static @Nullable View build(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneListener listener)
|
||||
{
|
||||
switch (megaphone.getStyle()) {
|
||||
case BASIC:
|
||||
return buildBasicMegaphone(context, megaphone, listener);
|
||||
case FULLSCREEN:
|
||||
return null;
|
||||
case REACTIONS:
|
||||
return buildReactionsMegaphone(context, megaphone, listener);
|
||||
default:
|
||||
throw new IllegalArgumentException("No view implemented for style!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull View buildBasicMegaphone(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneListener listener)
|
||||
{
|
||||
BasicMegaphoneView view = new BasicMegaphoneView(context);
|
||||
view.present(megaphone, listener);
|
||||
return view;
|
||||
}
|
||||
|
||||
private static @NonNull View buildReactionsMegaphone(@NonNull Context context,
|
||||
@NonNull Megaphone megaphone,
|
||||
@NonNull MegaphoneListener listener)
|
||||
{
|
||||
ReactionsMegaphoneView view = new ReactionsMegaphoneView(context);
|
||||
view.present(megaphone, listener);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Creating a new megaphone:
|
||||
* - Add an enum to {@link Event}
|
||||
* - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)}
|
||||
* - Include the event in {@link #buildDisplayOrder()}
|
||||
*
|
||||
* Common patterns:
|
||||
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}.
|
||||
* - For events guarded by feature flags, set a {@link ForeverSchedule} with false in
|
||||
* {@link #buildDisplayOrder()}.
|
||||
* - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)}
|
||||
* based on whatever properties you're interested in.
|
||||
*/
|
||||
public final class Megaphones {
|
||||
|
||||
private Megaphones() {}
|
||||
|
||||
static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
List<Megaphone> megaphones = Stream.of(buildDisplayOrder())
|
||||
.filter(e -> {
|
||||
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
|
||||
MegaphoneSchedule schedule = e.getValue();
|
||||
|
||||
return !record.isFinished() && schedule.shouldDisplay(record.getSeenCount(), record.getLastSeen(), record.getFirstVisible(), currentTime);
|
||||
})
|
||||
.map(Map.Entry::getKey)
|
||||
.map(records::get)
|
||||
.map(record -> Megaphones.forRecord(context, record))
|
||||
.toList();
|
||||
|
||||
boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory());
|
||||
boolean hasMandatory = Stream.of(megaphones).anyMatch(Megaphone::isMandatory);
|
||||
|
||||
if (hasOptional && hasMandatory) {
|
||||
megaphones = Stream.of(megaphones).filter(Megaphone::isMandatory).toList();
|
||||
}
|
||||
|
||||
if (megaphones.size() > 0) {
|
||||
return megaphones.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is when you would hide certain megaphones based on {@link FeatureFlags}. You could
|
||||
* conditionally set a {@link ForeverSchedule} set to false for disabled features.
|
||||
*/
|
||||
private static Map<Event, MegaphoneSchedule> buildDisplayOrder() {
|
||||
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
|
||||
put(Event.REACTIONS, new ForeverSchedule(true));
|
||||
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
|
||||
}};
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone forRecord(@NonNull Context context, @NonNull MegaphoneRecord record) {
|
||||
switch (record.getEvent()) {
|
||||
case REACTIONS:
|
||||
return buildReactionsMegaphone();
|
||||
case PINS_FOR_ALL:
|
||||
return buildPinsForAllMegaphone(context, record);
|
||||
default:
|
||||
throw new IllegalArgumentException("Event not handled!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildReactionsMegaphone() {
|
||||
return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS)
|
||||
.setMandatory(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull Context context, @NonNull MegaphoneRecord record) {
|
||||
if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) {
|
||||
return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN)
|
||||
.setMandatory(true)
|
||||
.enableSnooze(null)
|
||||
.setOnVisibleListener((megaphone, listener) -> {
|
||||
if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) {
|
||||
listener.onMegaphoneNavigationRequested(KbsMigrationActivity.createIntent(), KbsMigrationActivity.REQUEST_NEW_PIN);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
} else {
|
||||
Megaphone.Builder builder = new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC)
|
||||
.setMandatory(true)
|
||||
.setImage(R.drawable.kbs_pin_megaphone);
|
||||
|
||||
long daysRemaining = PinsForAllSchedule.getDaysRemaining(record.getFirstVisible(), System.currentTimeMillis());
|
||||
|
||||
if (PinUtil.userHasPin(ApplicationDependencies.getApplication())) {
|
||||
return buildPinsForAllMegaphoneForUserWithPin(
|
||||
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_confirming_your_pin, daysRemaining)))
|
||||
);
|
||||
} else {
|
||||
return buildPinsForAllMegaphoneForUserWithoutPin(
|
||||
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_creating_a_pin, daysRemaining)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) {
|
||||
return builder.setTitle(R.string.KbsMegaphone__introducing_pins)
|
||||
.setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin)
|
||||
.setButtonText(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> {
|
||||
Intent intent = CreateKbsPinActivity.getIntentForPinChangeFromSettings(ApplicationDependencies.getApplication());
|
||||
|
||||
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) {
|
||||
return builder.setTitle(R.string.KbsMegaphone__create_a_pin)
|
||||
.setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account)
|
||||
.setButtonText(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> {
|
||||
Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication());
|
||||
|
||||
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
REACTIONS("reactions"),
|
||||
PINS_FOR_ALL("pins_for_all");
|
||||
|
||||
private final String key;
|
||||
|
||||
Event(@NonNull String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public @NonNull String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public static Event fromKey(@NonNull String key) {
|
||||
for (Event event : values()) {
|
||||
if (event.getKey().equals(key)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No event for key: " + key);
|
||||
}
|
||||
|
||||
public static boolean hasKey(@NonNull String key) {
|
||||
for (Event event : values()) {
|
||||
if (event.getKey().equals(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class PinsForAllSchedule implements MegaphoneSchedule {
|
||||
|
||||
@VisibleForTesting
|
||||
static final long DAYS_UNTIL_FULLSCREEN = 8L;
|
||||
|
||||
@VisibleForTesting
|
||||
static final long DAYS_REMAINING_MAX = DAYS_UNTIL_FULLSCREEN - 1;
|
||||
|
||||
private final MegaphoneSchedule schedule = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
|
||||
|
||||
static boolean shouldDisplayFullScreen(long firstVisible, long currentTime) {
|
||||
if (pinCreationFailedDuringRegistration()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstVisible == 0L) {
|
||||
return false;
|
||||
} else {
|
||||
return currentTime - firstVisible >= TimeUnit.DAYS.toMillis(DAYS_UNTIL_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
static long getDaysRemaining(long firstVisible, long currentTime) {
|
||||
if (firstVisible == 0L) {
|
||||
return DAYS_REMAINING_MAX;
|
||||
} else {
|
||||
return Util.clamp(DAYS_REMAINING_MAX - TimeUnit.MILLISECONDS.toDays(currentTime - firstVisible), 0, DAYS_REMAINING_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
|
||||
if (!isEnabled()) return false;
|
||||
|
||||
if (shouldDisplayFullScreen(firstVisible, currentTime)) {
|
||||
return true;
|
||||
} else {
|
||||
return schedule.shouldDisplay(seenCount, lastSeen, firstVisible, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isEnabled() {
|
||||
if (FeatureFlags.pinsForAllMegaphoneKillSwitch()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pinCreationFailedDuringRegistration()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (newlyRegisteredV1PinUser()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SignalStore.registrationValues().pinWasRequiredAtRegistration()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return FeatureFlags.pinsForAll();
|
||||
}
|
||||
|
||||
private static boolean pinCreationFailedDuringRegistration() {
|
||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() &&
|
||||
!SignalStore.kbsValues().isV2RegistrationLockEnabled() &&
|
||||
!TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
|
||||
}
|
||||
|
||||
private static final boolean newlyRegisteredV1PinUser() {
|
||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() && TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
class RecurringSchedule implements MegaphoneSchedule {
|
||||
|
||||
private final long[] gaps;
|
||||
|
||||
RecurringSchedule(long... durationGaps) {
|
||||
this.gaps = durationGaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
|
||||
if (seenCount == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
long gap = gaps[Math.min(seenCount - 1, gaps.length - 1)];
|
||||
|
||||
return lastSeen + gap <= currentTime ;
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@ import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
@@ -56,12 +56,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException {
|
||||
if (!FeatureFlags.kbs()) {
|
||||
Log.i(TAG, "Not migrating pin to KBS");
|
||||
return;
|
||||
}
|
||||
|
||||
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException, KeyBackupSystemNoDataException {
|
||||
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
|
||||
Log.i(TAG, "Registration lock disabled");
|
||||
return;
|
||||
|
||||
@@ -123,7 +123,10 @@ public class IncomingLollipopMmsConnection extends LollipopMmsConnection impleme
|
||||
|
||||
Log.i(TAG, baos.size() + "-byte response: ");// + Hex.dump(baos.toByteArray()));
|
||||
|
||||
RetrieveConf retrieved = (RetrieveConf) new PduParser(baos.toByteArray()).parse();
|
||||
Bundle configValues = smsManager.getCarrierConfigValues();
|
||||
boolean parseContentDisposition = configValues.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION);
|
||||
|
||||
RetrieveConf retrieved = (RetrieveConf) new PduParser(baos.toByteArray(), parseContentDisposition).parse();
|
||||
|
||||
if (retrieved == null) return null;
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -550,7 +551,7 @@ public class MessageNotifier {
|
||||
if (KeyCachingService.isLocked(context)) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
||||
} else {
|
||||
String text = SpanUtil.italic(context.getString(R.string.MessageNotifier_reacted_to_your_message, EMOJI_REPLACEMENT_STRING)).toString();
|
||||
String text = SpanUtil.italic(getReactionMessageBody(context, record)).toString();
|
||||
String[] parts = text.split(EMOJI_REPLACEMENT_STRING);
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
@@ -580,6 +581,36 @@ public class MessageNotifier {
|
||||
return notificationState;
|
||||
}
|
||||
|
||||
private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
CharSequence body = record.getDisplayBody(context);
|
||||
boolean bodyIsEmpty = TextUtils.isEmpty(body);
|
||||
|
||||
if (MessageRecordUtil.hasSharedContact(record)) {
|
||||
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
|
||||
CharSequence summary = ContactUtil.getStringSummary(context, contact);
|
||||
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary);
|
||||
} else if (MessageRecordUtil.hasSticker(record)) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (record.isMms() && record.isViewOnce() && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_video, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (record.isMms() && record.isViewOnce()){
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_photo, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (!bodyIsEmpty) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body);
|
||||
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((MmsMessageRecord) record))) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((MmsMessageRecord) record))) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING);
|
||||
} else if (MessageRecordUtil.isMediaMessage(record)) {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING);
|
||||
} else {
|
||||
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body);
|
||||
}
|
||||
}
|
||||
|
||||
private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) {
|
||||
final String contentType = getMessageContentType(messageRecord);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
@@ -174,6 +175,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
break;
|
||||
case SUCCESS:
|
||||
TextSecurePreferences.setPushRegistered(getActivity(), false);
|
||||
SignalStore.registrationValues().clearRegistrationComplete();
|
||||
initializePushMessagingToggle();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.BlockedContactsActivity;
|
||||
@@ -21,10 +23,12 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -45,11 +49,27 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
|
||||
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
|
||||
|
||||
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
|
||||
regLock.setChecked(
|
||||
TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || SignalStore.kbsValues().isV2RegistrationLockEnabled()
|
||||
);
|
||||
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
|
||||
Preference kbsPinChange = this.findPreference(TextSecurePreferences.KBS_PIN_CHANGE);
|
||||
Preference regGroup = this.findPreference("prefs_lock_v1");
|
||||
Preference kbsGroup = this.findPreference("prefs_lock_v2");
|
||||
|
||||
if (FeatureFlags.pinsForAll()) {
|
||||
Preference preference = this.findPreference("pref_kbs_change");
|
||||
regGroup.setVisible(false);
|
||||
|
||||
if (PinUtil.userHasPin(ApplicationDependencies.getApplication())) {
|
||||
kbsPinChange.setOnPreferenceClickListener(new KbsPinUpdateListener());
|
||||
preference.setWidgetLayoutResource(R.layout.kbs_pin_change_preference);
|
||||
} else {
|
||||
kbsPinChange.setOnPreferenceClickListener(new KbsPinCreateListener());
|
||||
preference.setWidgetLayoutResource(R.layout.kbs_pin_create_preference);
|
||||
}
|
||||
} else {
|
||||
kbsGroup.setVisible(false);
|
||||
regLock.setChecked(PinUtil.userHasPin(requireContext()));
|
||||
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
}
|
||||
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
|
||||
@@ -84,6 +104,13 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
disablePassphrase.setChecked(!TextSecurePreferences.isPasswordDisabled(getActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePassphraseTimeoutSummary() {
|
||||
int timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(getActivity());
|
||||
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF)
|
||||
@@ -151,12 +178,28 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
}
|
||||
}
|
||||
|
||||
private class KbsPinUpdateListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivityForResult(CreateKbsPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class KbsPinCreateListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivityForResult(CreateKbsPinActivity.getIntentForPinCreate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Context context = requireContext();
|
||||
|
||||
if (TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
|
||||
if (PinUtil.userHasPin(context)) {
|
||||
RegistrationLockDialog.showRegistrationUnlockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
} else {
|
||||
RegistrationLockDialog.showRegistrationLockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
@@ -219,10 +262,11 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
|
||||
final int privacySummaryResId = FeatureFlags.pinsForAll() ? R.string.ApplicationPreferencesActivity_privacy_summary_screen_lock
|
||||
: R.string.ApplicationPreferencesActivity_privacy_summary;
|
||||
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
|
||||
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
|
||||
boolean registrationLockEnabled = TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
|
||||
boolean registrationLockEnabled = PinUtil.userHasPin(context);
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
|
||||
if (registrationLockEnabled) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -52,6 +51,8 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
.setOnPreferenceClickListener(new BackupClickListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
.setOnPreferenceClickListener(new BackupCreateListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_PASSPHRASE_VERIFY)
|
||||
.setOnPreferenceClickListener(new BackupVerifyListener());
|
||||
|
||||
initializeListSummary((ListPreference) findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF));
|
||||
|
||||
@@ -145,7 +146,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
|
||||
private class BackupCreateListener implements Preference.OnPreferenceClickListener {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Permissions.with(ChatsPreferenceFragment.this)
|
||||
@@ -162,6 +162,14 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupVerifyListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
BackupDialog.showVerifyBackupPassphraseDialog(requireContext());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaDownloadChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
|
||||
@@ -19,6 +19,7 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
|
||||
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";
|
||||
public static final String SHOW_TOOLBAR = "show_back_arrow";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
@@ -30,8 +31,10 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
|
||||
|
||||
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());
|
||||
if (bundle == null) {
|
||||
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
|
||||
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, getIntent().getExtras());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.NavDirections;
|
||||
@@ -34,6 +35,7 @@ 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.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -52,11 +54,15 @@ import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPL
|
||||
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;
|
||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.SHOW_TOOLBAR;
|
||||
|
||||
public class EditProfileFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(EditProfileFragment.class);
|
||||
private static final String TAG = Log.tag(EditProfileFragment.class);
|
||||
private static final String AVATAR_STATE = "avatar";
|
||||
|
||||
private Toolbar toolbar;
|
||||
private View title;
|
||||
private ImageView avatar;
|
||||
private CircularProgressButton finishButton;
|
||||
private EditText givenName;
|
||||
@@ -111,7 +117,7 @@ public class EditProfileFragment extends Fragment {
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeResources(view);
|
||||
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false));
|
||||
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), savedInstanceState != null);
|
||||
initializeProfileName();
|
||||
initializeProfileAvatar();
|
||||
initializeUsername();
|
||||
@@ -119,6 +125,20 @@ public class EditProfileFragment extends Fragment {
|
||||
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putByteArray(AVATAR_STATE, viewModel.getAvatarSnapshot());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(AVATAR_STATE)) {
|
||||
viewModel.setAvatar(savedInstanceState.getByteArray(AVATAR_STATE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
@@ -177,9 +197,9 @@ public class EditProfileFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeViewModel(boolean excludeSystem) {
|
||||
EditProfileRepository repository = new EditProfileRepository(requireContext(), excludeSystem);
|
||||
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository);
|
||||
private void initializeViewModel(boolean excludeSystem, boolean hasSavedInstanceState) {
|
||||
EditProfileRepository repository = new EditProfileRepository(requireContext(), excludeSystem);
|
||||
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository, hasSavedInstanceState);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(EditProfileViewModel.class);
|
||||
}
|
||||
@@ -187,6 +207,8 @@ public class EditProfileFragment extends Fragment {
|
||||
private void initializeResources(@NonNull View view) {
|
||||
Bundle arguments = requireArguments();
|
||||
|
||||
this.toolbar = view.findViewById(R.id.toolbar);
|
||||
this.title = view.findViewById(R.id.title);
|
||||
this.avatar = view.findViewById(R.id.avatar);
|
||||
this.givenName = view.findViewById(R.id.given_name);
|
||||
this.familyName = view.findViewById(R.id.family_name);
|
||||
@@ -231,20 +253,26 @@ public class EditProfileFragment extends Fragment {
|
||||
NavDirections action = EditProfileFragmentDirections.actionEditUsername();
|
||||
Navigation.findNavController(v).navigate(action);
|
||||
});
|
||||
|
||||
if (arguments.getBoolean(SHOW_TOOLBAR, true)) {
|
||||
this.toolbar.setVisibility(View.VISIBLE);
|
||||
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
|
||||
this.title.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeProfileName() {
|
||||
viewModel.profileName().observe(this, profileName -> {
|
||||
viewModel.givenName().observe(this, givenName -> updateFieldIfNeeded(this.givenName, givenName));
|
||||
|
||||
updateFieldIfNeeded(givenName, profileName.getGivenName());
|
||||
updateFieldIfNeeded(familyName, profileName.getFamilyName());
|
||||
viewModel.familyName().observe(this, familyName -> updateFieldIfNeeded(this.familyName, familyName));
|
||||
|
||||
viewModel.profileName().observe(this, profileName -> {
|
||||
preview.setText(profileName.toString());
|
||||
|
||||
boolean validEntry = !profileName.isGivenNameEmpty();
|
||||
|
||||
finishButton.setEnabled(validEntry);
|
||||
finishButton.setAlpha(validEntry ? 1f : 0.5f);
|
||||
|
||||
preview.setText(profileName.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,6 +322,9 @@ public class EditProfileFragment extends Fragment {
|
||||
Log.w(TAG, "Failed to delete capture file " + captureFile);
|
||||
}
|
||||
}
|
||||
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
|
||||
else handleFinishedLegacy();
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -22,19 +23,30 @@ class EditProfileViewModel extends ViewModel {
|
||||
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
|
||||
private final EditProfileRepository repository;
|
||||
|
||||
private EditProfileViewModel(@NonNull EditProfileRepository repository) {
|
||||
private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState) {
|
||||
this.repository = repository;
|
||||
|
||||
repository.getCurrentUsername(internalUsername::postValue);
|
||||
repository.getCurrentProfileName(name -> {
|
||||
givenName.setValue(name.getGivenName());
|
||||
familyName.setValue(name.getFamilyName());
|
||||
});
|
||||
repository.getCurrentAvatar(internalAvatar::setValue);
|
||||
|
||||
if (!hasInstanceState) {
|
||||
repository.getCurrentProfileName(name -> {
|
||||
givenName.setValue(name.getGivenName());
|
||||
familyName.setValue(name.getFamilyName());
|
||||
});
|
||||
repository.getCurrentAvatar(internalAvatar::setValue);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<String> givenName() {
|
||||
return Transformations.distinctUntilChanged(givenName);
|
||||
}
|
||||
|
||||
public LiveData<String> familyName() {
|
||||
return Transformations.distinctUntilChanged(familyName);
|
||||
}
|
||||
|
||||
public LiveData<ProfileName> profileName() {
|
||||
return internalProfileName;
|
||||
return Transformations.distinctUntilChanged(internalProfileName);
|
||||
}
|
||||
|
||||
public LiveData<byte[]> avatar() {
|
||||
@@ -49,6 +61,11 @@ class EditProfileViewModel extends ViewModel {
|
||||
return internalAvatar.getValue() != null;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public byte[] getAvatarSnapshot() {
|
||||
return internalAvatar.getValue();
|
||||
}
|
||||
|
||||
public void setGivenName(String givenName) {
|
||||
this.givenName.setValue(givenName);
|
||||
}
|
||||
@@ -73,16 +90,18 @@ class EditProfileViewModel extends ViewModel {
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final EditProfileRepository repository;
|
||||
private final boolean hasInstanceState;
|
||||
|
||||
Factory(EditProfileRepository repository) {
|
||||
this.repository = repository;
|
||||
Factory(@NonNull EditProfileRepository repository, boolean hasInstanceState) {
|
||||
this.repository = repository;
|
||||
this.hasInstanceState = hasInstanceState;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new EditProfileViewModel(repository);
|
||||
return (T) new EditProfileViewModel(repository, hasInstanceState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -19,6 +22,7 @@ import java.util.List;
|
||||
final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmojiCountAdapter.ViewHolder> {
|
||||
|
||||
private List<EmojiCount> emojiCountList = Collections.emptyList();
|
||||
private int totalCount = 0;
|
||||
private int selectedPosition = -1;
|
||||
|
||||
private final OnEmojiCountSelectedListener onEmojiCountSelectedListener;
|
||||
@@ -28,9 +32,10 @@ final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmoji
|
||||
}
|
||||
|
||||
void updateData(@NonNull List<EmojiCount> newEmojiCount) {
|
||||
if (selectedPosition != -1) {
|
||||
EmojiCount oldSelection = emojiCountList.get(selectedPosition);
|
||||
int newPosition = -1;
|
||||
if (selectedPosition != -1 && selectedPosition != 0) {
|
||||
int emojiPosition = selectedPosition - 1;
|
||||
EmojiCount oldSelection = emojiCountList.get(emojiPosition);
|
||||
int newPosition = -1;
|
||||
|
||||
for (int i = 0; i < newEmojiCount.size(); i++) {
|
||||
if (newEmojiCount.get(i).getEmoji().equals(oldSelection.getEmoji())) {
|
||||
@@ -41,17 +46,19 @@ final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmoji
|
||||
|
||||
if (newPosition == -1 && !newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
} else {
|
||||
selectedPosition = newPosition;
|
||||
selectedPosition = newPosition + 1;
|
||||
}
|
||||
} else if (!newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(newEmojiCount.get(0));
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
}
|
||||
|
||||
this.emojiCountList = newEmojiCount;
|
||||
|
||||
this.totalCount = Stream.of(emojiCountList).reduce(0, (sum, e) -> sum + e.getCount());
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -59,7 +66,7 @@ final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmoji
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item, parent, false), position -> {
|
||||
if (position != -1 && position != selectedPosition) {
|
||||
onEmojiCountSelectedListener.onSelected(emojiCountList.get(position));
|
||||
onEmojiCountSelectedListener.onSelected(position == 0 ? null : emojiCountList.get(position - 1).getEmoji());
|
||||
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = position;
|
||||
@@ -72,33 +79,44 @@ final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmoji
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(emojiCountList.get(position), selectedPosition);
|
||||
if (position == 0) {
|
||||
holder.bind(null, totalCount, selectedPosition == position);
|
||||
} else {
|
||||
EmojiCount item = emojiCountList.get(position - 1);
|
||||
holder.bind(item.getEmoji(), item.getCount(), selectedPosition == position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojiCountList.size();
|
||||
return 1 + emojiCountList.size();
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Drawable selected;
|
||||
private final Drawable selectedBackground;
|
||||
private final EmojiTextView emojiView;
|
||||
private final TextView countView;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
|
||||
ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text);
|
||||
selected = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
|
||||
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text );
|
||||
selectedBackground = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
|
||||
|
||||
itemView.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition()));
|
||||
}
|
||||
|
||||
void bind(@NonNull EmojiCount emojiCount, int selectedPosition) {
|
||||
emojiView.setText(emojiCount.getEmoji());
|
||||
countView.setText(String.valueOf(emojiCount.getCount()));
|
||||
itemView.setBackground(getAdapterPosition() == selectedPosition ? selected : null);
|
||||
void bind(@Nullable String emoji, int count, boolean selected) {
|
||||
if (emoji != null) {
|
||||
emojiView.setVisibility(View.VISIBLE);
|
||||
emojiView.setText(emoji);
|
||||
countView.setText(String.valueOf(count));
|
||||
} else {
|
||||
emojiView.setVisibility(View.GONE);
|
||||
countView.setText(itemView.getContext().getString(R.string.ReactionsBottomSheetDialogFragment_all, count));
|
||||
}
|
||||
itemView.setBackground(selected ? selectedBackground : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +125,6 @@ final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmoji
|
||||
}
|
||||
|
||||
interface OnEmojiCountSelectedListener {
|
||||
void onSelected(@NonNull EmojiCount emojiCount);
|
||||
void onSelected(@Nullable String emoji);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader.Reaction;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
||||
@@ -19,9 +20,9 @@ import java.util.List;
|
||||
|
||||
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
|
||||
|
||||
private List<Recipient> data = Collections.emptyList();
|
||||
private List<Reaction> data = Collections.emptyList();
|
||||
|
||||
public void updateData(List<Recipient> newData) {
|
||||
public void updateData(List<Reaction> newData) {
|
||||
data = newData;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -48,21 +49,24 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
private final TextView emoji;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
avatar = itemView.findViewById(R.id.reactions_bottom_view_recipient_avatar);
|
||||
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
|
||||
emoji = itemView.findViewById(R.id.reactions_bottom_view_recipient_emoji);
|
||||
}
|
||||
|
||||
void bind(Recipient recipient) {
|
||||
this.recipient.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
void bind(@NonNull Reaction reaction) {
|
||||
this.recipient.setText(reaction.getSender().getDisplayName(itemView.getContext()));
|
||||
this.emoji.setText(reaction.getEmoji());
|
||||
|
||||
if (recipient.equals(Recipient.self())) {
|
||||
AvatarUtil.loadIconIntoImageView(recipient, avatar);
|
||||
if (reaction.getSender().equals(Recipient.self())) {
|
||||
AvatarUtil.loadIconIntoImageView(reaction.getSender(), avatar);
|
||||
} else {
|
||||
this.avatar.setAvatar(GlideApp.with(avatar), recipient, false);
|
||||
this.avatar.setAvatar(GlideApp.with(avatar), reaction.getSender(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
}
|
||||
|
||||
private void setUpEmojiRecyclerView() {
|
||||
emojiCountAdapter = new ReactionEmojiCountAdapter((emojiCount -> viewModel.setFilterEmoji(emojiCount.getEmoji())));
|
||||
emojiCountAdapter = new ReactionEmojiCountAdapter((emoji -> viewModel.setFilterEmoji(emoji)));
|
||||
emojiRecyclerView.setAdapter(emojiCountAdapter);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ReactionsConversationView extends LinearLayout {
|
||||
|
||||
private static final int OUTER_MARGIN = ViewUtil.dpToPx(6);
|
||||
|
||||
private boolean outgoing;
|
||||
private List<ReactionRecord> records;
|
||||
|
||||
public ReactionsConversationView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public ReactionsConversationView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
records = new ArrayList<>();
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ReactionsConversationView, 0, 0);
|
||||
outgoing = typedArray.getBoolean(R.styleable.ReactionsConversationView_rcv_outgoing, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
removeAllViews();
|
||||
}
|
||||
|
||||
public void setReactions(@NonNull List<ReactionRecord> records, int bubbleWidth) {
|
||||
if (records.equals(this.records)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.records.clear();
|
||||
this.records.addAll(records);
|
||||
|
||||
List<Reaction> reactions = buildSortedReactionsList(records);
|
||||
|
||||
removeAllViews();
|
||||
|
||||
for (Reaction reaction : reactions) {
|
||||
addView(buildPill(getContext(), this, reaction));
|
||||
}
|
||||
|
||||
measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
||||
|
||||
int railWidth = getMeasuredWidth();
|
||||
|
||||
if (railWidth < (bubbleWidth - OUTER_MARGIN)) {
|
||||
int margin = (bubbleWidth - railWidth - OUTER_MARGIN);
|
||||
|
||||
if (outgoing) {
|
||||
ViewUtil.setRightMargin(this, margin);
|
||||
} else {
|
||||
ViewUtil.setLeftMargin(this, margin);
|
||||
}
|
||||
} else {
|
||||
if (outgoing) {
|
||||
ViewUtil.setRightMargin(this, OUTER_MARGIN);
|
||||
} else {
|
||||
ViewUtil.setLeftMargin(this, OUTER_MARGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<Reaction> buildSortedReactionsList(@NonNull List<ReactionRecord> records) {
|
||||
Map<String, Reaction> counters = new LinkedHashMap<>();
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
for (ReactionRecord record : records) {
|
||||
Reaction info = counters.get(record.getEmoji());
|
||||
|
||||
if (info == null) {
|
||||
info = new Reaction(record.getEmoji(), 1, record.getDateReceived(), selfId.equals(record.getAuthor()));
|
||||
} else {
|
||||
info.update(record.getDateReceived(), selfId.equals(record.getAuthor()));
|
||||
}
|
||||
|
||||
counters.put(record.getEmoji(), info);
|
||||
}
|
||||
|
||||
List<Reaction> reactions = new ArrayList<>(counters.values());
|
||||
|
||||
Collections.sort(reactions, Collections.reverseOrder());
|
||||
|
||||
if (reactions.size() > 3) {
|
||||
List<Reaction> shortened = new ArrayList<>(3);
|
||||
shortened.add(reactions.get(0));
|
||||
shortened.add(reactions.get(1));
|
||||
shortened.add(Stream.of(reactions).skip(2).reduce(new Reaction(null, 0, 0, false), Reaction::merge));
|
||||
|
||||
return shortened;
|
||||
} else {
|
||||
return reactions;
|
||||
}
|
||||
}
|
||||
|
||||
private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction) {
|
||||
View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false);
|
||||
TextView emojiView = root.findViewById(R.id.reactions_pill_emoji);
|
||||
TextView countView = root.findViewById(R.id.reactions_pill_count);
|
||||
View spacer = root.findViewById(R.id.reactions_pill_spacer);
|
||||
|
||||
if (reaction.emoji != null) {
|
||||
emojiView.setText(reaction.emoji);
|
||||
|
||||
if (reaction.count > 1) {
|
||||
countView.setText(String.valueOf(reaction.count));
|
||||
} else {
|
||||
countView.setVisibility(GONE);
|
||||
spacer.setVisibility(GONE);
|
||||
}
|
||||
} else {
|
||||
emojiView.setVisibility(GONE);
|
||||
spacer.setVisibility(GONE);
|
||||
countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count));
|
||||
}
|
||||
|
||||
if (reaction.userWasSender) {
|
||||
root.setBackground(ThemeUtil.getThemedDrawable(context, R.attr.reactions_pill_selected_background));
|
||||
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactions_pill_selected_text_color));
|
||||
} else {
|
||||
root.setBackground(ThemeUtil.getThemedDrawable(context, R.attr.reactions_pill_background));
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static class Reaction implements Comparable<Reaction> {
|
||||
private String emoji;
|
||||
private int count;
|
||||
private long lastSeen;
|
||||
private boolean userWasSender;
|
||||
|
||||
Reaction(@Nullable String emoji, int count, long lastSeen, boolean userWasSender) {
|
||||
this.emoji = emoji;
|
||||
this.count = count;
|
||||
this.lastSeen = lastSeen;
|
||||
this.userWasSender = userWasSender;
|
||||
}
|
||||
|
||||
void update(long lastSeen, boolean userWasSender) {
|
||||
this.count = this.count + 1;
|
||||
this.lastSeen = Math.max(this.lastSeen, lastSeen);
|
||||
this.userWasSender = this.userWasSender || userWasSender;
|
||||
}
|
||||
|
||||
@NonNull Reaction merge(@NonNull Reaction other) {
|
||||
this.count = this.count + other.count;
|
||||
this.lastSeen = Math.max(this.lastSeen, other.lastSeen);
|
||||
this.userWasSender = this.userWasSender || other.userWasSender;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Reaction rhs) {
|
||||
Reaction lhs = this;
|
||||
|
||||
if (lhs.count != rhs.count) {
|
||||
return Integer.compare(lhs.count, rhs.count);
|
||||
}
|
||||
|
||||
return Long.compare(lhs.lastSeen, rhs.lastSeen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneListener;
|
||||
|
||||
public class ReactionsMegaphoneView extends FrameLayout {
|
||||
|
||||
private View closeButton;
|
||||
|
||||
public ReactionsMegaphoneView(Context context) {
|
||||
super(context);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
public ReactionsMegaphoneView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(context);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull Context context) {
|
||||
inflate(context, R.layout.reactions_megaphone, this);
|
||||
|
||||
this.closeButton = findViewById(R.id.reactions_megaphone_x);
|
||||
}
|
||||
|
||||
public void present(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener) {
|
||||
this.closeButton.setOnClickListener(v -> listener.onMegaphoneCompleted(megaphone));
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -26,12 +23,12 @@ public class ReactionsViewModel extends ViewModel {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<Recipient>> getRecipients() {
|
||||
public @NonNull LiveData<List<Reaction>> getRecipients() {
|
||||
return Transformations.switchMap(filterEmoji,
|
||||
emoji -> Transformations.map(repository.getReactions(),
|
||||
reactions -> Stream.of(reactions)
|
||||
.filter(reaction -> reaction.getEmoji().equals(emoji))
|
||||
.map(Reaction::getSender).toList()));
|
||||
.filter(reaction -> emoji == null || reaction.getEmoji().equals(emoji))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
|
||||
|
||||
@@ -130,7 +130,7 @@ public class RecipientDetails {
|
||||
this.blocked = false;
|
||||
this.expireMessages = 0;
|
||||
this.participants = new LinkedList<>();
|
||||
this.profileName = null;
|
||||
this.profileName = ProfileName.EMPTY;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AccountLockedFragment extends BaseRegistrationFragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.account_locked_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
getModel().getTimeRemaining().observe(getViewLifecycleOwner(),
|
||||
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
|
||||
);
|
||||
|
||||
view.findViewById(R.id.account_locked_next).setOnClickListener(v -> onNext());
|
||||
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(v -> learnMore());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void learnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private static long durationToDays(Long duration) {
|
||||
return duration != null ? getLockoutDays(duration) : 7;
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onNext() {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
||||
@@ -121,25 +121,38 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
|
||||
model.setStorageCredentials(storageCredentials);
|
||||
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
|
||||
model.setTimeRemaining(timeRemaining);
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse triesRemaining) {
|
||||
// Unexpected, because at this point, no pin has been provided by the user.
|
||||
throw new AssertionError();
|
||||
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse tokenResponse, @NonNull String kbsStorageCredentials) {
|
||||
model.setTimeRemaining(timeRemaining);
|
||||
model.setStorageCredentials(kbsStorageCredentials);
|
||||
model.setKeyBackupCurrentToken(tokenResponse);
|
||||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTooManyAttempts() {
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
|
||||
throw new AssertionError("Unexpected, user has made no pin guesses");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRateLimited() {
|
||||
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
@@ -157,6 +170,14 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
|
||||
if (timeRemaining != null) {
|
||||
model.setTimeRemaining(timeRemaining);
|
||||
}
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
|
||||
@@ -14,7 +14,10 @@ import androidx.navigation.ActivityNavigator;
|
||||
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
||||
|
||||
@@ -30,17 +33,27 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
||||
|
||||
if (!isReregister()) {
|
||||
activity.startActivity(getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class)));
|
||||
final Intent main = new Intent(activity, MainActivity.class);
|
||||
final Intent next = chainIntents(new Intent(activity, EditProfileActivity.class), main);
|
||||
|
||||
next.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
|
||||
|
||||
Context context = requireContext();
|
||||
if (FeatureFlags.pinsForAll() && !PinUtil.userHasPin(context)) {
|
||||
activity.startActivity(chainIntents(CreateKbsPinActivity.getIntentForPinCreate(context), next));
|
||||
} else {
|
||||
activity.startActivity(next);
|
||||
}
|
||||
}
|
||||
|
||||
activity.finish();
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
|
||||
}
|
||||
|
||||
private static Intent getRoutedIntent(@NonNull Context context, Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(context, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
private static Intent chainIntents(@NonNull Intent sourceIntent, @Nullable Intent nextIntent) {
|
||||
if (nextIntent != null) sourceIntent.putExtra("next_intent", nextIntent);
|
||||
return sourceIntent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RegistrationLockFragment.class);
|
||||
|
||||
/** Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1. */
|
||||
private static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
private View forgotPin;
|
||||
private CircularProgressButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private TextView keyboardToggle;
|
||||
private long timeRemaining;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_registration_lock, container, false);
|
||||
}
|
||||
|
||||
@@ -47,38 +53,18 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
|
||||
|
||||
pinEntry = view.findViewById(R.id.pin);
|
||||
pinButton = view.findViewById(R.id.pinButton);
|
||||
|
||||
View clarificationLabel = view.findViewById(R.id.clarification_label);
|
||||
View subHeader = view.findViewById(R.id.verify_subheader);
|
||||
View pinForgotButton = view.findViewById(R.id.forgot_button);
|
||||
|
||||
String code = getModel().getTextCodeEntered();
|
||||
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
|
||||
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
|
||||
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
|
||||
forgotPin = view.findViewById(R.id.kbs_lock_forgot_pin);
|
||||
|
||||
timeRemaining = RegistrationLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
|
||||
|
||||
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.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) {
|
||||
boolean matchesTextCode = s != null && s.toString().equals(code);
|
||||
clarificationLabel.setVisibility(matchesTextCode ? View.VISIBLE : View.INVISIBLE);
|
||||
subHeader.setVisibility(matchesTextCode ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
});
|
||||
forgotPin.setVisibility(View.GONE);
|
||||
forgotPin.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
@@ -95,97 +81,141 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
model.getTokenResponseCredentialsPair()
|
||||
.observe(this, pair -> {
|
||||
TokenResponse token = pair.first();
|
||||
String credentials = pair.second();
|
||||
updateContinueText(token, credentials);
|
||||
});
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
model.onRegistrationLockFragmentCreate();
|
||||
}
|
||||
updateKeyboard(keyboardType.getOther());
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
private void updateContinueText(@Nullable TokenResponse tokenResponse, @Nullable String storageCredentials) {
|
||||
if (tokenResponse == null) {
|
||||
if (storageCredentials == null) {
|
||||
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue));
|
||||
} else {
|
||||
// TODO: This is the case where we can determine they are locked out
|
||||
// no token, but do have storage credentials. Might want to change text.
|
||||
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue));
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
|
||||
getModel().getTimeRemaining()
|
||||
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
|
||||
|
||||
TokenResponse keyBackupCurrentToken = getModel().getKeyBackupCurrentToken();
|
||||
|
||||
if (keyBackupCurrentToken != null) {
|
||||
int triesRemaining = keyBackupCurrentToken.getTries();
|
||||
if (triesRemaining <= 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
} else {
|
||||
int triesRemaining = tokenResponse.getTries();
|
||||
if (triesRemaining == 1) {
|
||||
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_last_attempt));
|
||||
} else {
|
||||
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_d_attempts_left, triesRemaining));
|
||||
|
||||
if (triesRemaining < 5) {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
}
|
||||
}
|
||||
pinButton.setText(pinButton.getIdleText());
|
||||
}
|
||||
|
||||
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
|
||||
Resources resources = requireContext().getResources();
|
||||
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
|
||||
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
|
||||
|
||||
return tries + " " + days;
|
||||
}
|
||||
|
||||
private PinKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void handlePinEntry() {
|
||||
final String pin = pinEntry.getText().toString();
|
||||
|
||||
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
|
||||
int trimmedLength = pin.replace(" ", "").length();
|
||||
if (trimmedLength == 0) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
String storageCredentials = model.getBasicStorageCredentials();
|
||||
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
|
||||
if (trimmedLength < MINIMUM_PIN_LENGTH) {
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
|
||||
String basicStorageCredentials = model.getBasicStorageCredentials();
|
||||
|
||||
setSpinning(pinButton);
|
||||
|
||||
registrationService.verifyAccount(requireActivity(),
|
||||
model.getFcmToken(),
|
||||
model.getTextCodeEntered(),
|
||||
pin, storageCredentials, tokenResponse,
|
||||
pin,
|
||||
basicStorageCredentials,
|
||||
tokenResponse,
|
||||
|
||||
new CodeVerificationRequest.VerifyCallback() {
|
||||
|
||||
@Override
|
||||
public void onSuccessfulRegistration() {
|
||||
cancelSpinning(pinButton);
|
||||
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
|
||||
model.setStorageCredentials(storageCredentials);
|
||||
cancelSpinning(pinButton);
|
||||
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
|
||||
getModel().setTimeRemaining(timeRemaining);
|
||||
|
||||
pinEntry.setText("");
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_registration_lock_pin, Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse kbsTokenResponse, @NonNull String kbsStorageCredentials) {
|
||||
throw new AssertionError("Not expected after a pin guess");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
|
||||
model.setKeyBackupCurrentToken(tokenResponse);
|
||||
|
||||
int triesRemaining = tokenResponse.getTries();
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
handleForgottenPin(timeRemaining);
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_pin_incorrect)
|
||||
.setMessage(getString(R.string.RegistrationActivity_you_have_d_tries_remaining, triesRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
if (triesRemaining == 3) {
|
||||
int daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
|
||||
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
|
||||
forgotPin.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTooManyAttempts() {
|
||||
public void onRateLimited() {
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
@@ -195,6 +225,15 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
|
||||
if (timeRemaining != null) {
|
||||
model.setTimeRemaining(timeRemaining);
|
||||
}
|
||||
|
||||
onAccountLocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
cancelSpinning(pinButton);
|
||||
@@ -205,10 +244,36 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
int lockoutDays = getLockoutDays(timeRemainingMs);
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_oh_no)
|
||||
.setMessage(getString(R.string.RegistrationActivity_registration_of_this_phone_number_will_be_possible_without_your_registration_lock_pin_after_seven_days_have_passed, (TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1)))
|
||||
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
|
||||
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static int getLockoutDays(long timeRemainingMs) {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void onAccountLocked() {
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
}
|
||||
|
||||
private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.RegistrationLockFragment__enter_alphanumeric_pin;
|
||||
} else {
|
||||
return R.string.RegistrationLockFragment__enter_numeric_pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
FAILURE_UNKNOWN
|
||||
}
|
||||
|
||||
private static class PassphraseAsYouTypeFormatter implements TextWatcher {
|
||||
public static class PassphraseAsYouTypeFormatter implements TextWatcher {
|
||||
|
||||
private static final int GROUP_SIZE = 5;
|
||||
|
||||
@@ -292,7 +292,7 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
addSpans(editable);
|
||||
}
|
||||
|
||||
private void removeSpans(Editable editable) {
|
||||
private static void removeSpans(Editable editable) {
|
||||
SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class);
|
||||
|
||||
for (SpaceSpan span : paddingSpans) {
|
||||
@@ -300,15 +300,15 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void addSpans(Editable editable) {
|
||||
private static void addSpans(Editable editable) {
|
||||
final int length = editable.length();
|
||||
|
||||
for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) {
|
||||
editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
if (editable.length() > 30) {
|
||||
editable.delete(30, editable.length());
|
||||
if (editable.length() > BackupUtil.PASSPHRASE_LENGTH) {
|
||||
editable.delete(BackupUtil.PASSPHRASE_LENGTH, editable.length());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
@@ -36,6 +35,7 @@ import org.whispersystems.libsignal.util.KeyHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
@@ -53,16 +53,12 @@ public final class CodeVerificationRequest {
|
||||
|
||||
private static final String TAG = Log.tag(CodeVerificationRequest.class);
|
||||
|
||||
static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
|
||||
if (basicStorageCredentials == null) return null;
|
||||
return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials);
|
||||
}
|
||||
|
||||
private enum Result {
|
||||
SUCCESS,
|
||||
PIN_LOCKED,
|
||||
KBS_WRONG_PIN,
|
||||
RATE_LIMITED,
|
||||
KBS_ACCOUNT_LOCKED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -87,17 +83,42 @@ public final class CodeVerificationRequest {
|
||||
{
|
||||
new AsyncTask<Void, Void, Result>() {
|
||||
|
||||
private volatile LockedException lockedException;
|
||||
private volatile KeyBackupSystemWrongPinException keyBackupSystemWrongPinException;
|
||||
private volatile LockedException lockedException;
|
||||
private volatile TokenResponse kbsToken;
|
||||
|
||||
@Override
|
||||
protected Result doInBackground(Void... voids) {
|
||||
final boolean pinSupplied = pin != null;
|
||||
final boolean tryKbs = kbsTokenResponse != null;
|
||||
|
||||
try {
|
||||
verifyAccount(context, credentials, code, pin, basicStorageCredentials, kbsTokenResponse, fcmToken);
|
||||
kbsToken = kbsTokenResponse;
|
||||
verifyAccount(context, credentials, code, pin, kbsTokenResponse, basicStorageCredentials, fcmToken);
|
||||
return Result.SUCCESS;
|
||||
} catch (KeyBackupSystemNoDataException e) {
|
||||
Log.w(TAG, "No data found on KBS");
|
||||
return Result.KBS_ACCOUNT_LOCKED;
|
||||
} catch (KeyBackupSystemWrongPinException e) {
|
||||
kbsToken = e.getTokenResponse();
|
||||
return Result.KBS_WRONG_PIN;
|
||||
} catch (LockedException e) {
|
||||
if (pinSupplied && tryKbs) {
|
||||
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
|
||||
}
|
||||
|
||||
Log.w(TAG, e);
|
||||
lockedException = e;
|
||||
if (e.getBasicStorageCredentials() != null) {
|
||||
try {
|
||||
kbsToken = getToken(e.getBasicStorageCredentials());
|
||||
if (kbsToken == null || kbsToken.getTries() == 0) {
|
||||
return Result.KBS_ACCOUNT_LOCKED;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Log.w(TAG, e);
|
||||
return Result.ERROR;
|
||||
}
|
||||
}
|
||||
return Result.PIN_LOCKED;
|
||||
} catch (RateLimitException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -105,9 +126,6 @@ public final class CodeVerificationRequest {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return Result.ERROR;
|
||||
} catch (KeyBackupSystemWrongPinException e) {
|
||||
keyBackupSystemWrongPinException = e;
|
||||
return Result.KBS_WRONG_PIN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,22 +137,41 @@ public final class CodeVerificationRequest {
|
||||
callback.onSuccessfulRegistration();
|
||||
break;
|
||||
case PIN_LOCKED:
|
||||
callback.onIncorrectRegistrationLockPin(lockedException.getTimeRemaining(), lockedException.getBasicStorageCredentials());
|
||||
if (kbsToken != null) {
|
||||
if (lockedException.getBasicStorageCredentials() == null) {
|
||||
throw new AssertionError("KBS Token set, but no storage credentials supplied.");
|
||||
}
|
||||
Log.w(TAG, "Reg Locked: V2 pin needed for registration");
|
||||
callback.onKbsRegistrationLockPinRequired(lockedException.getTimeRemaining(), kbsToken, lockedException.getBasicStorageCredentials());
|
||||
} else {
|
||||
Log.w(TAG, "Reg Locked: V1 pin needed for registration");
|
||||
callback.onV1RegistrationLockPinRequiredOrIncorrect(lockedException.getTimeRemaining());
|
||||
}
|
||||
break;
|
||||
case RATE_LIMITED:
|
||||
callback.onTooManyAttempts();
|
||||
callback.onRateLimited();
|
||||
break;
|
||||
case ERROR:
|
||||
callback.onError();
|
||||
break;
|
||||
case KBS_WRONG_PIN:
|
||||
callback.onIncorrectKbsRegistrationLockPin(keyBackupSystemWrongPinException.getTokenResponse());
|
||||
Log.w(TAG, "KBS Pin was wrong");
|
||||
callback.onIncorrectKbsRegistrationLockPin(kbsToken);
|
||||
break;
|
||||
case KBS_ACCOUNT_LOCKED:
|
||||
Log.w(TAG, "KBS Account is locked");
|
||||
callback.onKbsAccountLocked(lockedException != null ? lockedException.getTimeRemaining() : null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
|
||||
if (basicStorageCredentials == null) return null;
|
||||
return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials);
|
||||
}
|
||||
|
||||
private static void handleSuccessfulRegistration(@NonNull Context context) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
jobManager.add(new DirectoryRefreshJob(false));
|
||||
@@ -148,11 +185,12 @@ public final class CodeVerificationRequest {
|
||||
@NonNull Credentials credentials,
|
||||
@NonNull String code,
|
||||
@Nullable String pin,
|
||||
@Nullable String basicStorageCredentials,
|
||||
@Nullable TokenResponse kbsTokenResponse,
|
||||
@Nullable String kbsStorageCredentials,
|
||||
@Nullable String fcmToken)
|
||||
throws IOException, KeyBackupSystemWrongPinException
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
boolean isV2KbsPin = kbsTokenResponse != null;
|
||||
int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
@@ -161,10 +199,10 @@ public final class CodeVerificationRequest {
|
||||
SessionUtil.archiveAllSessions(context);
|
||||
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
|
||||
RegistrationLockData kbsData = restoreMasterKey(pin, basicStorageCredentials, kbsTokenResponse);
|
||||
RegistrationLockData kbsData = isV2KbsPin ? restoreMasterKey(pin, kbsStorageCredentials, kbsTokenResponse) : null;
|
||||
String registrationLock = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;
|
||||
boolean present = fcmToken != null;
|
||||
String pinForServer = basicStorageCredentials == null ? pin : null;
|
||||
String pinForServer = isV2KbsPin ? null : pin;
|
||||
|
||||
UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present,
|
||||
pinForServer, registrationLock,
|
||||
@@ -212,12 +250,6 @@ public final class CodeVerificationRequest {
|
||||
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pin);
|
||||
//noinspection deprecation Only acceptable place to write the old pin enabled state.
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, pin != null);
|
||||
if (pin != null) {
|
||||
if (FeatureFlags.kbs()) {
|
||||
Log.i(TAG, "Pin V1 successfully entered during registration, scheduling a migration to Pin V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SignalStore.kbsValues().setRegistrationLockMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
repostPinToResetTries(context, pin, kbsData);
|
||||
@@ -251,14 +283,13 @@ public final class CodeVerificationRequest {
|
||||
|
||||
private static @Nullable RegistrationLockData restoreMasterKey(@Nullable String pin,
|
||||
@Nullable String basicStorageCredentials,
|
||||
@Nullable TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException
|
||||
@NonNull TokenResponse tokenResponse)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
if (pin == null) return null;
|
||||
|
||||
if (basicStorageCredentials == null) {
|
||||
Log.i(TAG, "No storage credentials supplied, pin is not on KBS");
|
||||
return null;
|
||||
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
|
||||
}
|
||||
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
@@ -273,7 +304,7 @@ public final class CodeVerificationRequest {
|
||||
if (kbsData != null) {
|
||||
Log.i(TAG, "Found registration lock token on KBS.");
|
||||
} else {
|
||||
Log.i(TAG, "No KBS data found.");
|
||||
throw new AssertionError("Null not expected");
|
||||
}
|
||||
return kbsData;
|
||||
} catch (UnauthenticatedResponseException e) {
|
||||
@@ -290,13 +321,32 @@ public final class CodeVerificationRequest {
|
||||
void onSuccessfulRegistration();
|
||||
|
||||
/**
|
||||
* The account is locked with a V1 (non-KBS) pin.
|
||||
*
|
||||
* @param timeRemaining Time until pin expires and number can be reused.
|
||||
*/
|
||||
void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials);
|
||||
void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining);
|
||||
|
||||
/**
|
||||
* The account is locked with a V2 (KBS) pin. Called before any user pin guesses.
|
||||
*/
|
||||
void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse kbsTokenResponse, @NonNull String kbsStorageCredentials);
|
||||
|
||||
/**
|
||||
* The account is locked with a V2 (KBS) pin. Called after a user pin guess.
|
||||
* <p>
|
||||
* i.e. an attempt has likely been used.
|
||||
*/
|
||||
void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse kbsTokenResponse);
|
||||
|
||||
void onTooManyAttempts();
|
||||
/**
|
||||
* V2 (KBS) pin is set, but there is no data on KBS.
|
||||
*
|
||||
* @param timeRemaining Non-null if known.
|
||||
*/
|
||||
void onKbsAccountLocked(@Nullable Long timeRemaining);
|
||||
|
||||
void onRateLimited();
|
||||
|
||||
void onError();
|
||||
}
|
||||
|
||||
@@ -45,8 +45,4 @@ public final class RegistrationService {
|
||||
{
|
||||
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, basicStorageCredentials, tokenResponse, callback);
|
||||
}
|
||||
|
||||
public @Nullable TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
|
||||
return CodeVerificationRequest.getToken(basicStorageCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,10 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
@@ -34,8 +29,7 @@ public final class RegistrationViewModel extends ViewModel {
|
||||
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
|
||||
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
|
||||
private final MutableLiveData<String> keyBackupcurrentTokenJson;
|
||||
private final LiveData<TokenResponse> keyBackupcurrentToken;
|
||||
private final LiveData<Pair<TokenResponse, String>> tokenResponseCredentialsPair;
|
||||
private final MutableLiveData<Long> timeRemaining;
|
||||
|
||||
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
||||
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
|
||||
@@ -49,19 +43,7 @@ public final class RegistrationViewModel extends ViewModel {
|
||||
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
|
||||
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
|
||||
keyBackupcurrentTokenJson = savedStateHandle.getLiveData("KBS_TOKEN");
|
||||
|
||||
keyBackupcurrentToken = Transformations.map(keyBackupcurrentTokenJson, json ->
|
||||
{
|
||||
if (json == null) return null;
|
||||
try {
|
||||
return JsonUtil.fromJson(json, TokenResponse.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
tokenResponseCredentialsPair = new LiveDataPair<>(keyBackupcurrentToken, basicStorageCredentials);
|
||||
timeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L);
|
||||
}
|
||||
|
||||
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
|
||||
@@ -179,26 +161,26 @@ public final class RegistrationViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public @Nullable TokenResponse getKeyBackupCurrentToken() {
|
||||
return keyBackupcurrentToken.getValue();
|
||||
String json = keyBackupcurrentTokenJson.getValue();
|
||||
if (json == null) return null;
|
||||
try {
|
||||
return JsonUtil.fromJson(json, TokenResponse.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeyBackupCurrentToken(TokenResponse tokenResponse) {
|
||||
keyBackupcurrentTokenJson.setValue(tokenResponse == null ? null : JsonUtil.toJson(tokenResponse));
|
||||
String json = tokenResponse == null ? null : JsonUtil.toJson(tokenResponse);
|
||||
keyBackupcurrentTokenJson.setValue(json);
|
||||
}
|
||||
|
||||
public LiveData<Pair<TokenResponse, String>> getTokenResponseCredentialsPair() {
|
||||
return tokenResponseCredentialsPair;
|
||||
public LiveData<Long> getTimeRemaining() {
|
||||
return timeRemaining;
|
||||
}
|
||||
|
||||
public void onRegistrationLockFragmentCreate() {
|
||||
SimpleTask.run(() -> {
|
||||
RegistrationService registrationService = RegistrationService.getInstance(getNumber().getE164Number(), getRegistrationSecret());
|
||||
try {
|
||||
return registrationService.getToken(getBasicStorageCredentials());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}, this::setKeyBackupCurrentToken);
|
||||
public void setTimeRemaining(long timeRemaining) {
|
||||
this.timeRemaining.setValue(timeRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public class BackupUtil {
|
||||
|
||||
private static final String TAG = BackupUtil.class.getSimpleName();
|
||||
|
||||
public static final int PASSPHRASE_LENGTH = 30;
|
||||
|
||||
public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
|
||||
try {
|
||||
BackupInfo backup = getLatestBackup();
|
||||
|
||||
@@ -46,13 +46,13 @@ public final class FeatureFlags {
|
||||
private static final String PREFIX = "android.";
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
private static final String UUIDS = generateKey("uuids");
|
||||
private static final String PROFILE_DISPLAY = generateKey("profileDisplay");
|
||||
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
|
||||
private static final String USERNAMES = generateKey("usernames");
|
||||
private static final String KBS = generateKey("kbs");
|
||||
private static final String STORAGE_SERVICE = generateKey("storageService");
|
||||
private static final String REACTION_SENDING = generateKey("reactionSending");
|
||||
private static final String UUIDS = generateKey("uuids");
|
||||
private static final String PROFILE_DISPLAY = generateKey("profileDisplay");
|
||||
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
|
||||
private static final String USERNAMES = generateKey("usernames");
|
||||
private static final String STORAGE_SERVICE = generateKey("storageService");
|
||||
private static final String PINS_FOR_ALL = generateKey("beta.pinsForAll"); // TODO [alex] remove beta prefix
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = generateKey("pinsMegaphoneKillSwitch");
|
||||
|
||||
/**
|
||||
* Values in this map will take precedence over any value. If you do not wish to have any sort of
|
||||
@@ -75,13 +75,14 @@ public final class FeatureFlags {
|
||||
* more burden on the reader to ensure that the app experience remains consistent.
|
||||
*/
|
||||
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
|
||||
KBS
|
||||
PINS_MEGAPHONE_KILL_SWITCH
|
||||
);
|
||||
|
||||
/**
|
||||
* Flags in this set will stay true forever once they receive a true value from a remote config.
|
||||
*/
|
||||
private static final Set<String> STICKY = Sets.newHashSet(
|
||||
PINS_FOR_ALL // TODO [alex] -- add android.beta.pinsForAll to sticky set when we remove prefix
|
||||
);
|
||||
|
||||
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
|
||||
@@ -141,21 +142,19 @@ public final class FeatureFlags {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Set or migrate PIN to KBS */
|
||||
public static boolean kbs() {
|
||||
return getValue(KBS, false);
|
||||
}
|
||||
|
||||
/** Storage service. Requires {@link #kbs()}. */
|
||||
/** Storage service. */
|
||||
public static boolean storageService() {
|
||||
boolean value = getValue(STORAGE_SERVICE, false);
|
||||
if (value && !kbs()) throw new MissingFlagRequirementError();
|
||||
return value;
|
||||
return getValue(STORAGE_SERVICE, false);
|
||||
}
|
||||
|
||||
/** Send support for reactions. */
|
||||
public static synchronized boolean reactionSending() {
|
||||
return getValue(REACTION_SENDING, false);
|
||||
/** Enables new KBS UI and notices but does not require user to set a pin */
|
||||
public static boolean pinsForAll() {
|
||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() || getValue(PINS_FOR_ALL, false);
|
||||
}
|
||||
|
||||
/** Safety flag to disable Pins for All Megaphone */
|
||||
public static boolean pinsForAllMegaphoneKillSwitch() {
|
||||
return getValue(PINS_MEGAPHONE_KILL_SWITCH, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
public final class RequestCodes {
|
||||
|
||||
public static final int NOT_SET = -1;
|
||||
|
||||
private RequestCodes() { }
|
||||
}
|
||||
@@ -150,6 +150,7 @@ public class TextSecurePreferences {
|
||||
private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase";
|
||||
private static final String BACKUP_TIME = "pref_backup_next_time";
|
||||
public static final String BACKUP_NOW = "pref_backup_create";
|
||||
public static final String BACKUP_PASSPHRASE_VERIFY = "pref_backup_passphrase_verify";
|
||||
|
||||
public static final String SCREEN_LOCK = "pref_android_screen_lock";
|
||||
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
|
||||
@@ -159,10 +160,11 @@ public class TextSecurePreferences {
|
||||
@Deprecated
|
||||
private static final String REGISTRATION_LOCK_PIN_PREF_V1 = "pref_registration_lock_pin";
|
||||
|
||||
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time";
|
||||
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS = "pref_registration_lock_last_reminder_time_post_kbs";
|
||||
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
|
||||
|
||||
public static final String KBS_PIN_CHANGE = "pref_kbs_change";
|
||||
|
||||
private static final String SERVICE_OUTAGE = "pref_service_outage";
|
||||
private static final String LAST_OUTAGE_CHECK_TIME = "pref_last_outage_check_time";
|
||||
|
||||
@@ -269,16 +271,11 @@ public class TextSecurePreferences {
|
||||
}
|
||||
|
||||
public static long getRegistrationLockLastReminderTime(@NonNull Context context) {
|
||||
return getLongPreference(context, getAppropriateReminderKey(), 0);
|
||||
return getLongPreference(context, REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS, 0);
|
||||
}
|
||||
|
||||
public static void setRegistrationLockLastReminderTime(@NonNull Context context, long time) {
|
||||
setLongPreference(context, getAppropriateReminderKey(), time);
|
||||
}
|
||||
|
||||
private static String getAppropriateReminderKey() {
|
||||
return FeatureFlags.kbs() ? REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS
|
||||
: REGISTRATION_LOCK_LAST_REMINDER_TIME;
|
||||
setLongPreference(context, REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS, time);
|
||||
}
|
||||
|
||||
public static long getRegistrationLockNextReminderInterval(@NonNull Context context) {
|
||||
|
||||
@@ -509,6 +509,10 @@ public class Util {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static long clamp(long value, long min, long max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static float clamp(float value, float min, float max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
@@ -233,6 +233,16 @@ public class ViewUtil {
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
public static void setRightMargin(@NonNull View view, int margin) {
|
||||
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin;
|
||||
} else {
|
||||
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin;
|
||||
}
|
||||
view.forceLayout();
|
||||
view.requestLayout();
|
||||
}
|
||||
|
||||
public static void setTopMargin(@NonNull View view, int margin) {
|
||||
((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin;
|
||||
view.requestLayout();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user