diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dccf47a9cf..c10eb6052b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -475,7 +475,12 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> - + + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index c2d8a2c986..c902c0e19a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; +import org.thoughtcrime.securesms.pin.PinRestoreActivity; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -32,15 +33,17 @@ import java.util.Locale; public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener { private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName(); - public static final String LOCALE_EXTRA = "locale_extra"; + public static final String LOCALE_EXTRA = "locale_extra"; + public static final String NEXT_INTENT_EXTRA = "next_intent"; private static final int STATE_NORMAL = 0; private static final int STATE_CREATE_PASSPHRASE = 1; private static final int STATE_PROMPT_PASSPHRASE = 2; private static final int STATE_UI_BLOCKING_UPGRADE = 3; private static final int STATE_WELCOME_PUSH_SCREEN = 4; - private static final int STATE_CREATE_PROFILE_NAME = 5; - private static final int STATE_CREATE_KBS_PIN = 6; + private static final int STATE_ENTER_SIGNAL_PIN = 5; + private static final int STATE_CREATE_PROFILE_NAME = 6; + private static final int STATE_CREATE_SIGNAL_PIN = 7; private SignalServiceNetworkAccess networkAccess; private BroadcastReceiver clearKeyReceiver; @@ -155,7 +158,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent(); case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent(); case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent(); - case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent(); + case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent(); + case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent(); case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent(); default: return null; } @@ -170,21 +174,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA return STATE_UI_BLOCKING_UPGRADE; } else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) { return STATE_WELCOME_PUSH_SCREEN; + } else if (SignalStore.storageServiceValues().needsAccountRestore()) { + return STATE_ENTER_SIGNAL_PIN; } else if (userMustSetProfileName()) { return STATE_CREATE_PROFILE_NAME; - } else if (userMustSetKbsPin()) { - return STATE_CREATE_KBS_PIN; + } else if (userMustCreateSignalPin()) { + return STATE_CREATE_SIGNAL_PIN; } else { return STATE_NORMAL; } } - private boolean userMustSetKbsPin() { + private boolean userMustCreateSignalPin() { return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin(); } private boolean userMustSetProfileName() { - return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName() == ProfileName.EMPTY; + return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty(); } private Intent getCreatePassphraseIntent() { @@ -206,7 +212,11 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA return RegistrationNavigationActivity.newIntentForNewRegistration(this); } - private Intent getCreateKbsPinIntent() { + private Intent getEnterSignalPinIntent() { + return getRoutedIntent(PinRestoreActivity.class, getIntent()); + } + + private Intent getCreateSignalPinIntent() { final Intent intent; if (userMustSetProfileName()) { @@ -252,4 +262,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA clearKeyReceiver = null; } } + + /** + * Puts an extra in {@code intent} so that {@code nextIntent} will be shown after it. + */ + public static @NonNull Intent chainIntent(@NonNull Intent intent, @NonNull Intent nextIntent) { + intent.putExtra(NEXT_INTENT_EXTRA, nextIntent); + return intent; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java index 57de235f4f..3ee37b4409 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.IntentUtils; import org.thoughtcrime.securesms.util.text.AfterTextChanged; @@ -148,19 +149,10 @@ public class HelpFragment extends Fragment { .map(view -> Feeling.getByViewId(view.getId())) .findFirst().orElse(null); - Spanned body = getEmailBody(debugLog, feeling); - - Intent intent = new Intent(Intent.ACTION_SENDTO); - intent.setData(Uri.parse("mailto:")); - intent.putExtra(Intent.EXTRA_EMAIL, new String[]{getString(R.string.RegistrationActivity_support_email)}); - intent.putExtra(Intent.EXTRA_SUBJECT, getEmailSubject()); - intent.putExtra(Intent.EXTRA_TEXT, body.toString()); - - if (IntentUtils.isResolvable(requireContext(), intent)) { - startActivity(intent); - } else { - Toast.makeText(requireContext(), R.string.HelpFragment__no_email_app_found, Toast.LENGTH_LONG).show(); - } + CommunicationActions.openEmail(requireContext(), + getString(R.string.RegistrationActivity_support_email), + getEmailSubject(), + getEmailBody(debugLog, feeling).toString()); } private String getEmailSubject() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index a1d70739f5..bea14b1741 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -49,6 +49,11 @@ public class RefreshAttributesJob extends BaseJob { @Override public void onRun() throws IOException { + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.w(TAG, "Not yet registered. Skipping."); + return; + } + int registrationId = TextSecurePreferences.getLocalRegistrationId(context); boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context); byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java index 27d3df9d7d..bd31efe1e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java @@ -6,7 +6,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.util.Map; @@ -14,6 +16,8 @@ import java.util.concurrent.TimeUnit; public class RemoteConfigRefreshJob extends BaseJob { + private static final String TAG = Log.tag(RemoteConfigRefreshJob.class); + public static final String KEY = "RemoteConfigRefreshJob"; public RemoteConfigRefreshJob() { @@ -41,6 +45,11 @@ public class RemoteConfigRefreshJob extends BaseJob { @Override protected void onRun() throws Exception { + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.w(TAG, "Not registered. Skipping."); + return; + } + Map config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig(); FeatureFlags.update(config); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java index d3944ae7e3..856a0c39f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java @@ -51,6 +51,11 @@ public class RotateCertificateJob extends BaseJob { @Override public void onRun() throws IOException { + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.w(TAG, "Not yet registered. Ignoring."); + return; + } + synchronized (RotateCertificateJob.class) { SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); byte[] certificate = accountManager.getSenderCertificate(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java index 281f3b27cc..b2b7c06fee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; @@ -33,7 +34,7 @@ public class StorageAccountRestoreJob extends BaseJob { public static String KEY = "StorageAccountRestoreJob"; - public static long LIFESPAN = TimeUnit.SECONDS.toMillis(10); + public static long LIFESPAN = TimeUnit.SECONDS.toMillis(20); private static final String TAG = Log.tag(StorageAccountRestoreJob.class); @@ -69,7 +70,8 @@ public class StorageAccountRestoreJob extends BaseJob { Optional manifest = accountManager.getStorageManifest(storageServiceKey); if (!manifest.isPresent()) { - Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring."); + Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring. Force-pushing."); + ApplicationDependencies.getJobManager().add(new StorageForcePushJob()); return; } @@ -97,16 +99,13 @@ public class StorageAccountRestoreJob extends BaseJob { StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId()); StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord); + JobManager jobManager = ApplicationDependencies.getJobManager(); + if (accountRecord.getAvatarUrlPath().isPresent()) { - RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()); - try { - avatarJob.setContext(context); - avatarJob.onRun(); - } catch (IOException e) { - Log.w(TAG, "Failed to download avatar. Scheduling for later."); - ApplicationDependencies.getJobManager().add(avatarJob); - } + jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN/2); } + + jobManager.runSynchronously(new RefreshAttributesJob(), LIFESPAN/2); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index 93df306804..dfae83570d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import com.annimon.stream.Stream; -import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncModels; @@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncValidations; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.InvalidKeyException; @@ -78,20 +78,26 @@ public class StorageForcePushJob extends BaseJob { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context); - long currentVersion = accountManager.getStorageManifestVersion(); - Map oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap(); + long currentVersion = accountManager.getStorageManifestVersion(); + Map oldContactStorageIds = recipientDatabase.getContactStorageSyncIdsMap(); - long newVersion = currentVersion + 1; - Map newStorageKeys = generateNewKeys(oldStorageKeys); - Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); - List inserts = Stream.of(oldStorageKeys.keySet()) - .map(recipientDatabase::getRecipientSettings) - .withoutNulls() - .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients)) - .toList(); - inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId()))); + long newVersion = currentVersion + 1; + Map newContactStorageIds = generateContactStorageIds(oldContactStorageIds); + Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); + List inserts = Stream.of(oldContactStorageIds.keySet()) + .map(recipientDatabase::getRecipientSettings) + .withoutNulls() + .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients)) + .toList(); - SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values())); + SignalStorageRecord accountRecord = StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId())); + List allNewStorageIds = new ArrayList<>(newContactStorageIds.values()); + + inserts.add(accountRecord); + allNewStorageIds.add(accountRecord.getId()); + + SignalStorageManifest manifest = new SignalStorageManifest(newVersion, allNewStorageIds); + StorageSyncValidations.validateForcePush(manifest, inserts); try { if (newVersion > 1) { @@ -114,7 +120,8 @@ public class StorageForcePushJob extends BaseJob { Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion); TextSecurePreferences.setStorageManifestVersion(context, newVersion); - recipientDatabase.applyStorageIdUpdates(newStorageKeys); + recipientDatabase.applyStorageIdUpdates(newContactStorageIds); + recipientDatabase.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId())); storageKeyDatabase.deleteAll(); } @@ -127,7 +134,7 @@ public class StorageForcePushJob extends BaseJob { public void onFailure() { } - private static @NonNull Map generateNewKeys(@NonNull Map oldKeys) { + private static @NonNull Map generateContactStorageIds(@NonNull Map oldKeys) { Map out = new HashMap<>(); for (Map.Entry entry : oldKeys.entrySet()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index b50ebca69d..d2ab64668d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -28,6 +28,8 @@ public final class KbsValues { /** * Deliberately does not clear the {@link #MASTER_KEY}. + * + * Should only be called by {@link org.thoughtcrime.securesms.pin.PinState} */ public void clearRegistrationLockAndPin() { store.beginWrite() @@ -37,6 +39,7 @@ public final class KbsValues { .commit(); } + /** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */ public synchronized void setKbsMasterKey(@NonNull KbsPinData pinData, @NonNull String localPinHash) { MasterKey masterKey = pinData.getMasterKey(); String tokenResponse; @@ -53,6 +56,7 @@ public final class KbsValues { .commit(); } + /** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */ public synchronized void setV2RegistrationLockEnabled(boolean enabled) { store.beginWrite().putBoolean(V2_LOCK_ENABLED, enabled).apply(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java index fbcce8af17..828d646c5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PinValues.java @@ -85,6 +85,7 @@ public final class PinValues { return PinKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null)); } + /** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState} */ public void setPinState(@NonNull String pinState) { store.beginWrite().putString(PIN_STATE, pinState).commit(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java index 676ae73b53..1c48457dea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java @@ -10,7 +10,8 @@ import java.security.SecureRandom; public class StorageServiceValues { - private static final String LAST_SYNC_TIME = "storage.last_sync_time"; + private static final String LAST_SYNC_TIME = "storage.last_sync_time"; + private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore"; private final KeyValueStore store; @@ -29,4 +30,12 @@ public class StorageServiceValues { public void onSyncCompleted() { store.beginWrite().putLong(LAST_SYNC_TIME, System.currentTimeMillis()).apply(); } + + public boolean needsAccountRestore() { + return store.getBoolean(NEEDS_ACCOUNT_RESTORE, false); + } + + public void setNeedsAccountRestore(boolean value) { + store.beginWrite().putBoolean(NEEDS_ACCOUNT_RESTORE, value).apply(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockV1Dialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockV1Dialog.java index 0a7a75d796..ce2e30d8e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockV1Dialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockV1Dialog.java @@ -180,7 +180,7 @@ public final class RegistrationLockV1Dialog { protected Boolean doInBackground(Void... voids) { try { Log.i(TAG, "Setting pin on KBS - dialog"); - PinState.onCompleteRegistrationLockV1Reminder(context, pinValue); + PinState.onEnableLegacyRegistrationLockPreference(context, pinValue); Log.i(TAG, "Pin set on KBS"); return true; } catch (IOException | UnauthenticatedResponseException e) { @@ -235,7 +235,7 @@ public final class RegistrationLockV1Dialog { @Override protected Boolean doInBackground(Void... voids) { try { - PinState.onDisableRegistrationLockV1(context); + PinState.onDisableLegacyRegistrationLockPreference(context); return true; } catch (IOException | UnauthenticatedResponseException e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java index 5d14f84a9d..2cd8b2b17a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java @@ -20,6 +20,8 @@ import org.thoughtcrime.securesms.animation.AnimationRepeatListener; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.megaphone.Megaphones; +import org.thoughtcrime.securesms.registration.RegistrationUtil; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.SpanUtil; import java.util.Objects; @@ -109,7 +111,8 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment= 28) { add(new LogSectionPower()); } + add(new LogSectionPin()); add(new LogSectionThreads()); add(new LogSectionFeatureFlags()); add(new LogSectionPermissions()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java index 5067fd22c3..b133df9df5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java @@ -42,7 +42,7 @@ class PinsForAllSchedule implements MegaphoneSchedule { } private static boolean isEnabled() { - if (SignalStore.kbsValues().isV2RegistrationLockEnabled()) { + if (SignalStore.kbsValues().hasPin()) { return false; } @@ -54,7 +54,7 @@ class PinsForAllSchedule implements MegaphoneSchedule { return true; } - if (newlyRegisteredV1PinUser()) { + if (newlyRegisteredRegistrationLockV1User()) { return true; } @@ -67,11 +67,11 @@ class PinsForAllSchedule implements MegaphoneSchedule { private static boolean pinCreationFailedDuringRegistration() { return SignalStore.registrationValues().pinWasRequiredAtRegistration() && - !SignalStore.kbsValues().isV2RegistrationLockEnabled() && + !SignalStore.kbsValues().hasPin() && !TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication()); } - private static boolean newlyRegisteredV1PinUser() { + private static boolean newlyRegisteredRegistrationLockV1User() { return SignalStore.registrationValues().pinWasRequiredAtRegistration() && TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java index 15ee515fb2..18d29b0e5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/RegistrationPinV2MigrationJob.java @@ -13,6 +13,7 @@ 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.pin.PinState; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.KeyBackupServicePinException; @@ -59,7 +60,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob { } @Override - protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException, KeyBackupSystemNoDataException { + protected void onRun() throws IOException, UnauthenticatedResponseException { if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) { Log.i(TAG, "Registration lock disabled"); return; @@ -74,19 +75,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob { } Log.i(TAG, "Migrating pin to Key Backup Service"); - - KbsValues kbsValues = SignalStore.kbsValues(); - MasterKey masterKey = kbsValues.getOrCreateMasterKey(); - KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); - KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession); - KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); - - pinChangeSession.enableRegistrationLock(masterKey); - - kbsValues.setKbsMasterKey(kbsData, PinHashing.localPinHash(pinValue)); - TextSecurePreferences.clearRegistrationLockV1(context); - + PinState.onMigrateToRegistrationLockV2(context, pinValue); Log.i(TAG, "Pin migrated to Key Backup Service"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreActivity.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreActivity.java new file mode 100644 index 0000000000..6ca6334fbe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreActivity.java @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms.pin; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import org.thoughtcrime.securesms.MainActivity; +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; + +public final class PinRestoreActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.pin_restore_activity); + } + + void navigateToPinCreation() { + final Intent main = new Intent(this, MainActivity.class); + final Intent createPin = CreateKbsPinActivity.getIntentForPinCreate(this); + final Intent chained = PassphraseRequiredActionBarActivity.chainIntent(createPin, main); + + startActivity(chained); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java new file mode 100644 index 0000000000..14a67c68c4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java @@ -0,0 +1,290 @@ +package org.thoughtcrime.securesms.pin; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +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.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.Navigation; + +import com.dd.CircularProgressButton; + +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.MainActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.lock.v2.KbsConstants; +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; +import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.util.Locale; + +public class PinRestoreEntryFragment extends Fragment { + private static final String TAG = Log.tag(PinRestoreActivity.class); + + private static final int MINIMUM_PIN_LENGTH = 4; + + private EditText pinEntry; + private View helpButton; + private View skipButton; + private CircularProgressButton pinButton; + private TextView errorLabel; + private TextView keyboardToggle; + private PinRestoreViewModel viewModel; + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.pin_restore_entry_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + initViews(view); + initViewModel(); + } + + private void initViews(@NonNull View root) { + pinEntry = root.findViewById(R.id.pin_restore_pin_input); + pinButton = root.findViewById(R.id.pin_restore_pin_confirm); + errorLabel = root.findViewById(R.id.pin_restore_pin_input_label); + keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle); + helpButton = root.findViewById(R.id.pin_restore_forgot_pin); + skipButton = root.findViewById(R.id.pin_restore_skip_button); + + helpButton.setVisibility(View.GONE); + helpButton.setOnClickListener(v -> onNeedHelpClicked()); + + skipButton.setOnClickListener(v -> onSkipClicked()); + + pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE); + pinEntry.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + ViewUtil.hideKeyboard(requireContext(), v); + onPinSubmitted(); + return true; + } + return false; + }); + + enableAndFocusPinEntry(); + + pinButton.setOnClickListener((v) -> { + ViewUtil.hideKeyboard(requireContext(), pinEntry); + onPinSubmitted(); + }); + + keyboardToggle.setOnClickListener((v) -> { + PinKeyboardType keyboardType = getPinEntryKeyboardType(); + + updateKeyboard(keyboardType.getOther()); + keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + }); + + PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther(); + keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + } + + private void initViewModel() { + viewModel = ViewModelProviders.of(this).get(PinRestoreViewModel.class); + + viewModel.getTriesRemaining().observe(this, this::presentTriesRemaining); + viewModel.getEvent().observe(this, this::presentEvent); + } + + private void presentTriesRemaining(PinRestoreViewModel.TriesRemaining triesRemaining) { + if (triesRemaining.hasIncorrectGuess()) { + if (triesRemaining.getCount() == 1) { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_incorrect_pin) + .setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount())) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + + errorLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin); + helpButton.setVisibility(View.VISIBLE); + skipButton.setVisibility(View.VISIBLE); + } else { + if (triesRemaining.getCount() == 1) { + helpButton.setVisibility(View.VISIBLE); + new AlertDialog.Builder(requireContext()) + .setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount())) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } + + if (triesRemaining.getCount() == 0) { + Log.w(TAG, "Account locked. User out of attempts on KBS."); + onAccountLocked(); + } + } + + private void presentEvent(@NonNull PinRestoreViewModel.Event event) { + switch (event) { + case SUCCESS: + handleSuccess(); + break; + case EMPTY_PIN: + Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show(); + cancelSpinning(pinButton); + pinEntry.getText().clear(); + enableAndFocusPinEntry(); + break; + case PIN_TOO_SHORT: + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show(); + cancelSpinning(pinButton); + pinEntry.getText().clear(); + enableAndFocusPinEntry(); + break; + case PIN_INCORRECT: + cancelSpinning(pinButton); + pinEntry.getText().clear(); + enableAndFocusPinEntry(); + break; + case PIN_LOCKED: + onAccountLocked(); + break; + case NETWORK_ERROR: + Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); + cancelSpinning(pinButton); + pinEntry.setEnabled(true); + enableAndFocusPinEntry(); + break; + } + } + + private PinKeyboardType getPinEntryKeyboardType() { + boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER; + + return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC; + } + + private void onPinSubmitted() { + pinEntry.setEnabled(false); + viewModel.onPinSubmitted(pinEntry.getText().toString(), getPinEntryKeyboardType()); + setSpinning(pinButton); + } + + private void onNeedHelpClicked() { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_need_help) + .setMessage(getString(R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code, KbsConstants.MINIMUM_PIN_LENGTH)) + .setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> { + CommunicationActions.openEmail(requireContext(), + getString(R.string.PinRestoreEntryFragment_support_email), + getString(R.string.PinRestoreEntryFragment_signal_registration_need_help_with_pin), + getString(R.string.PinRestoreEntryFragment_subject_signal_registration, + getDevice(), + getAndroidVersion(), + BuildConfig.VERSION_NAME, + Locale.getDefault())); + }) + .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) + .show(); + } + + private void onSkipClicked() { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry) + .setMessage(R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin) + .setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, (dialog, which) -> { + PinState.onPinRestoreForgottenOrSkipped(); + ((PinRestoreActivity) requireActivity()).navigateToPinCreation(); + }) + .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) + .show(); + } + + private void onAccountLocked() { + Navigation.findNavController(requireView()).navigate(PinRestoreEntryFragmentDirections.actionAccountLocked()); + } + + private void handleSuccess() { + cancelSpinning(pinButton); + + Activity activity = requireActivity(); + + if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) { + final Intent main = new Intent(activity, MainActivity.class); + final Intent profile = EditProfileActivity.getIntent(activity, false); + + profile.putExtra("next_intent", main); + startActivity(profile); + } else { + startActivity(new Intent(activity, MainActivity.class)); + } + + activity.finish(); + } + + 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.PinRestoreEntryFragment_enter_alphanumeric_pin; + } else { + return R.string.PinRestoreEntryFragment_enter_numeric_pin; + } + } + + private void enableAndFocusPinEntry() { + pinEntry.setEnabled(true); + pinEntry.setFocusable(true); + + if (pinEntry.requestFocus()) { + ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0); + } + } + + private static void setSpinning(@Nullable CircularProgressButton button) { + if (button != null) { + button.setClickable(false); + button.setIndeterminateProgressMode(true); + button.setProgress(50); + } + } + + private static void cancelSpinning(@Nullable CircularProgressButton button) { + if (button != null) { + button.setProgress(0); + button.setIndeterminateProgressMode(false); + button.setClickable(true); + } + } + + private static String getDevice() { + return String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.PRODUCT); + } + + private static String getAndroidVersion() { + return String.format("%s (%s, %s)", Build.VERSION.RELEASE, Build.VERSION.INCREMENTAL, Build.DISPLAY); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreLockedFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreLockedFragment.java new file mode 100644 index 0000000000..e4c8c83c61 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreLockedFragment.java @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.pin; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.CommunicationActions; + +public class PinRestoreLockedFragment extends Fragment { + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.pin_restore_locked_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + View createPinButton = view.findViewById(R.id.pin_locked_next); + View learnMoreButton = view.findViewById(R.id.pin_locked_learn_more); + + createPinButton.setOnClickListener(v -> { + PinState.onPinRestoreForgottenOrSkipped(); + ((PinRestoreActivity) requireActivity()).navigateToPinCreation(); + }); + + learnMoreButton.setOnClickListener(v -> { + CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)); + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java new file mode 100644 index 0000000000..2211e83685 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java @@ -0,0 +1,106 @@ +package org.thoughtcrime.securesms.pin; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException; +import org.thoughtcrime.securesms.util.Stopwatch; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.Executor; + +class PinRestoreRepository { + + private static final String TAG = Log.tag(PinRestoreRepository.class); + + private final Executor executor = SignalExecutors.UNBOUNDED; + private final KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(); + + void getToken(@NonNull Callback> callback) { + executor.execute(() -> { + try { + String authorization = kbs.getAuthorization(); + TokenResponse token = kbs.getToken(authorization); + TokenData tokenData = new TokenData(authorization, token); + callback.onComplete(Optional.of(tokenData)); + } catch (IOException e) { + callback.onComplete(Optional.absent()); + } + }); + } + + void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback callback) { + executor.execute(() -> { + try { + Stopwatch stopwatch = new Stopwatch("PinSubmission"); + + KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.basicAuth, tokenData.tokenResponse); + PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin); + stopwatch.split("MasterKey"); + + ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); + stopwatch.split("AccountRestore"); + + stopwatch.stop(TAG); + + callback.onComplete(new PinResultData(PinResult.SUCCESS, tokenData)); + } catch (IOException e) { + callback.onComplete(new PinResultData(PinResult.NETWORK_ERROR, tokenData)); + } catch (KeyBackupSystemNoDataException e) { + callback.onComplete(new PinResultData(PinResult.LOCKED, tokenData)); + } catch (KeyBackupSystemWrongPinException e) { + callback.onComplete(new PinResultData(PinResult.INCORRECT, new TokenData(tokenData.basicAuth, e.getTokenResponse()))); + } + }); + } + + interface Callback { + void onComplete(@NonNull T value); + } + + static class TokenData { + private final String basicAuth; + private final TokenResponse tokenResponse; + + TokenData(@NonNull String basicAuth, @NonNull TokenResponse tokenResponse) { + this.basicAuth = basicAuth; + this.tokenResponse = tokenResponse; + } + + int getTriesRemaining() { + return tokenResponse.getTries(); + } + } + + static class PinResultData { + private final PinResult result; + private final TokenData tokenData; + + PinResultData(@NonNull PinResult result, @NonNull TokenData tokenData) { + this.result = result; + this.tokenData = tokenData; + } + + public @NonNull PinResult getResult() { + return result; + } + + public @NonNull TokenData getTokenData() { + return tokenData; + } + } + + enum PinResult { + SUCCESS, INCORRECT, LOCKED, NETWORK_ERROR + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java new file mode 100644 index 0000000000..e2e6e1b890 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.pin; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.v2.KbsConstants; +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; +import org.thoughtcrime.securesms.util.DefaultValueLiveData; +import org.thoughtcrime.securesms.util.SingleLiveEvent; + +public class PinRestoreViewModel extends ViewModel { + + private final PinRestoreRepository repo; + private final DefaultValueLiveData triesRemaining; + private final SingleLiveEvent event; + + private volatile PinRestoreRepository.TokenData tokenData; + + public PinRestoreViewModel() { + this.repo = new PinRestoreRepository(); + this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false)); + this.event = new SingleLiveEvent<>(); + + repo.getToken(token -> { + if (token.isPresent()) { + updateTokenData(token.get(), false); + } else { + event.postValue(Event.NETWORK_ERROR); + } + }); + } + + void onPinSubmitted(@NonNull String pin, @NonNull PinKeyboardType pinKeyboardType) { + int trimmedLength = pin.replace(" ", "").length(); + + if (trimmedLength == 0) { + event.postValue(Event.EMPTY_PIN); + return; + } + + if (trimmedLength < KbsConstants.MINIMUM_PIN_LENGTH) { + event.postValue(Event.PIN_TOO_SHORT); + return; + } + + if (tokenData != null) { + repo.submitPin(pin, tokenData, result -> { + + switch (result.getResult()) { + case SUCCESS: + SignalStore.pinValues().setKeyboardType(pinKeyboardType); + SignalStore.storageServiceValues().setNeedsAccountRestore(false); + event.postValue(Event.SUCCESS); + break; + case LOCKED: + event.postValue(Event.PIN_LOCKED); + break; + case INCORRECT: + event.postValue(Event.PIN_INCORRECT); + updateTokenData(result.getTokenData(), true); + break; + case NETWORK_ERROR: + event.postValue(Event.NETWORK_ERROR); + break; + } + }); + } else { + repo.getToken(token -> { + if (token.isPresent()) { + updateTokenData(token.get(), false); + onPinSubmitted(pin, pinKeyboardType); + } else { + event.postValue(Event.NETWORK_ERROR); + } + }); + } + } + + @NonNull DefaultValueLiveData getTriesRemaining() { + return triesRemaining; + } + + @NonNull LiveData getEvent() { + return event; + } + + private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) { + this.tokenData = tokenData; + triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess)); + } + + enum Event { + SUCCESS, EMPTY_PIN, PIN_TOO_SHORT, PIN_INCORRECT, PIN_LOCKED, NETWORK_ERROR + } + + static class TriesRemaining { + private final int triesRemaining; + private final boolean hasIncorrectGuess; + + TriesRemaining(int triesRemaining, boolean hasIncorrectGuess) { + this.triesRemaining = triesRemaining; + this.hasIncorrectGuess = hasIncorrectGuess; + } + + public int getCount() { + return triesRemaining; + } + + public boolean hasIncorrectGuess() { + return hasIncorrectGuess; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index ef2fe2fc05..581597670f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import java.io.IOException; +import java.util.Arrays; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -84,35 +85,63 @@ public final class PinState { */ public static synchronized void onRegistration(@NonNull Context context, @Nullable KbsPinData kbsData, - @Nullable String pin) + @Nullable String pin, + boolean hasPinToRestore) { Log.i(TAG, "onNewRegistration()"); - if (kbsData == null) { - Log.i(TAG, "No KBS PIN. Clearing any PIN state."); - SignalStore.kbsValues().clearRegistrationLockAndPin(); - //noinspection deprecation Only acceptable place to write the old pin. - TextSecurePreferences.setV1RegistrationLockPin(context, pin); - //noinspection deprecation Only acceptable place to write the old pin enabled state. - TextSecurePreferences.setV1RegistrationLockEnabled(context, pin != null); - } else { - Log.i(TAG, "Had a KBS PIN. Saving data."); - SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin)); - // TODO [greyson] [pins] Not always true -- when this flow is reworked, you can have a PIN but no reglock - SignalStore.kbsValues().setV2RegistrationLockEnabled(true); - resetPinRetryCount(context, pin, kbsData); - } + TextSecurePreferences.setV1RegistrationLockPin(context, pin); - if (pin != null) { + if (kbsData == null && pin != null) { + Log.i(TAG, "Registration Lock V1"); + SignalStore.kbsValues().clearRegistrationLockAndPin(); + TextSecurePreferences.setV1RegistrationLockEnabled(context, true); TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); + } else if (kbsData != null && pin != null) { + Log.i(TAG, "Registration Lock V2"); + TextSecurePreferences.setV1RegistrationLockEnabled(context, false); + SignalStore.kbsValues().setV2RegistrationLockEnabled(true); + SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin)); SignalStore.pinValues().resetPinReminders(); - ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); + resetPinRetryCount(context, pin, kbsData); + } else if (hasPinToRestore) { + Log.i(TAG, "Has a PIN to restore."); + SignalStore.kbsValues().clearRegistrationLockAndPin(); + TextSecurePreferences.setV1RegistrationLockEnabled(context, false); + SignalStore.storageServiceValues().setNeedsAccountRestore(true); + } else { + Log.i(TAG, "No registration lock or PIN at all."); + SignalStore.kbsValues().clearRegistrationLockAndPin(); + TextSecurePreferences.setV1RegistrationLockEnabled(context, false); } updateState(buildInferredStateFromOtherFields()); } + /** + * Invoked when the user is going through the PIN restoration flow (which is separate from reglock). + */ + public static synchronized void onSignalPinRestore(@NonNull Context context, @NonNull KbsPinData kbsData, @NonNull String pin) { + SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin)); + SignalStore.kbsValues().setV2RegistrationLockEnabled(false); + SignalStore.pinValues().resetPinReminders(); + SignalStore.storageServiceValues().setNeedsAccountRestore(false); + resetPinRetryCount(context, pin, kbsData); + + updateState(buildInferredStateFromOtherFields()); + } + + /** + * Invoked when the user skips out on PIN restoration or otherwise fails to remember their PIN. + */ + public static synchronized void onPinRestoreForgottenOrSkipped() { + SignalStore.kbsValues().clearRegistrationLockAndPin(); + SignalStore.storageServiceValues().setNeedsAccountRestore(false); + + updateState(buildInferredStateFromOtherFields()); + } + /** * Invoked whenever the Signal PIN is changed or created. */ @@ -181,10 +210,11 @@ public final class PinState { } /** - * Invoked whenever registration lock is disabled for a user without a Signal PIN. + * Called when registration lock is disabled in the settings using the old UI (i.e. no mention of + * Signal PINs). */ @WorkerThread - public static synchronized void onDisableRegistrationLockV1(@NonNull Context context) + public static synchronized void onDisableLegacyRegistrationLockPreference(@NonNull Context context) throws IOException, UnauthenticatedResponseException { Log.i(TAG, "onDisableRegistrationLockV1()"); @@ -197,12 +227,16 @@ public final class PinState { updateState(State.NO_REGISTRATION_LOCK); } + /** + * Called when registration lock is enabled in the settings using the old UI (i.e. no mention of + * Signal PINs). + */ @WorkerThread - public static synchronized void onCompleteRegistrationLockV1Reminder(@NonNull Context context, @NonNull String pin) + public static synchronized void onEnableLegacyRegistrationLockPreference(@NonNull Context context, @NonNull String pin) throws IOException, UnauthenticatedResponseException { Log.i(TAG, "onCompleteRegistrationLockV1Reminder()"); - assertState(State.REGISTRATION_LOCK_V1); + assertState(State.NO_REGISTRATION_LOCK); KbsValues kbsValues = SignalStore.kbsValues(); MasterKey masterKey = kbsValues.getOrCreateMasterKey(); @@ -219,7 +253,29 @@ public final class PinState { TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); - updateState(State.PIN_WITH_REGISTRATION_LOCK_ENABLED); + updateState(buildInferredStateFromOtherFields()); + } + + /** + * Should only be called by {@link org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob}. + */ + @WorkerThread + public static synchronized void onMigrateToRegistrationLockV2(@NonNull Context context, @NonNull String pin) + throws IOException, UnauthenticatedResponseException + { + KbsValues kbsValues = SignalStore.kbsValues(); + MasterKey masterKey = kbsValues.getOrCreateMasterKey(); + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); + KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + + pinChangeSession.enableRegistrationLock(masterKey); + + kbsValues.setKbsMasterKey(kbsData, PinHashing.localPinHash(pin)); + TextSecurePreferences.clearRegistrationLockV1(context); + + updateState(buildInferredStateFromOtherFields()); } public static synchronized boolean shouldShowRegistrationLockV1Reminder() { @@ -273,7 +329,7 @@ public final class PinState { } } - throw new IllegalStateException(); + throw new IllegalStateException("Expected: " + Arrays.toString(allowed) + ", Actual: " + currentState); } private static @NonNull State getState() { @@ -289,6 +345,7 @@ public final class PinState { } private static void updateState(@NonNull State state) { + Log.i(TAG, "Updating state to: " + state); SignalStore.pinValues().setPinState(state.serialize()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index d919517482..12790a0152 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -81,7 +81,8 @@ public class AvatarHelper { * Whether or not an avatar is present for the given recipient. */ public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) { - return getAvatarFile(context, recipientId).exists(); + File avatarFile = getAvatarFile(context, recipientId); + return avatarFile.exists() && avatarFile.length() > 0; } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java index 6d98106a46..2a7526c683 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/ProfileName.java @@ -34,8 +34,7 @@ public final class ProfileName implements Parcelable { this(in.readString(), in.readString()); } - public @NonNull - String getGivenName() { + public @NonNull String getGivenName() { return givenName; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java index 610814c71c..0388aba00b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java @@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.profiles.edit; import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.navigation.NavGraph; import androidx.navigation.Navigation; @@ -23,6 +26,12 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme(); + public static @NonNull Intent getIntent(@NonNull Context context, boolean showToolbar) { + Intent intent = new Intent(context, EditProfileActivity.class); + intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, showToolbar); + return intent; + } + @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java index 2c3b590ae0..bfb51e0630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java @@ -45,6 +45,8 @@ import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.registration.RegistrationUtil; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.text.AfterTextChanged; @@ -307,9 +309,7 @@ public class EditProfileFragment extends Fragment { private void handleUpload() { viewModel.submitProfile(uploadResult -> { if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) { - if (SignalStore.kbsValues().hasPin()) { - SignalStore.registrationValues().setRegistrationComplete(); - } + RegistrationUtil.markRegistrationPossiblyComplete(); ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PROFILE_NAMES_FOR_ALL); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java new file mode 100644 index 0000000000..43ed573847 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.registration; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; + +public final class RegistrationUtil { + + private static final String TAG = Log.tag(RegistrationUtil.class); + + private RegistrationUtil() {} + + /** + * There's several events where a registration may or may not be considered complete based on what + * path a user has taken. This will only truly mark registration as complete if all of the + * requirements are met. + */ + public static void markRegistrationPossiblyComplete() { + if (SignalStore.kbsValues().hasPin() && !Recipient.self().getProfileName().isEmpty()) { + Log.i(TAG, "Marking registration completed.", new Throwable()); + SignalStore.registrationValues().setRegistrationComplete(); + StorageSyncHelper.scheduleSyncForDataChange(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java index e0dd81b9c9..575a7a4e35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest; import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; import org.thoughtcrime.securesms.registration.service.RegistrationService; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; +import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; @@ -305,16 +306,14 @@ public final class EnterCodeFragment extends BaseRegistrationFragment { } private void sendEmailToSupport() { - Intent intent = new Intent(Intent.ACTION_SENDTO); - intent.setData(Uri.parse("mailto:")); - intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ getString(R.string.RegistrationActivity_support_email) }); - intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.RegistrationActivity_code_support_subject)); - intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.RegistrationActivity_code_support_body, - getDevice(), - getAndroidVersion(), - BuildConfig.VERSION_NAME, - Locale.getDefault())); - startActivity(intent); + CommunicationActions.openEmail(requireContext(), + getString(R.string.RegistrationActivity_support_email), + getString(R.string.RegistrationActivity_code_support_subject), + getString(R.string.RegistrationActivity_code_support_body, + getDevice(), + getAndroidVersion(), + BuildConfig.VERSION_NAME, + Locale.getDefault())); } private static String getDevice() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java index d360fb82b1..1d42a2f2ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java @@ -13,7 +13,9 @@ import androidx.navigation.ActivityNavigator; import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; +import org.thoughtcrime.securesms.pin.PinRestoreActivity; import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; public final class RegistrationCompleteFragment extends BaseRegistrationFragment { @@ -30,12 +32,11 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment FragmentActivity activity = requireActivity(); - - if (!isReregister()) { + if (SignalStore.storageServiceValues().needsAccountRestore()) { + activity.startActivity(new Intent(activity, PinRestoreActivity.class)); + } else if (!isReregister()) { final Intent main = new Intent(activity, MainActivity.class); - final Intent profile = new Intent(activity, EditProfileActivity.class); - - profile.putExtra(EditProfileActivity.SHOW_TOOLBAR, false); + final Intent profile = EditProfileActivity.getIntent(activity, false); Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext()); activity.startActivity(chainIntents(chainIntents(profile, kbs), main)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index 8a36554178..b6bd280e84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -39,8 +39,10 @@ import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; import java.util.List; @@ -210,16 +212,18 @@ public final class CodeVerificationRequest { Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2)); - UUID uuid = accountManager.verifyAccountWithCode(code, - null, - registrationId, - !hasFcm, - registrationLockV1, - registrationLockV2, - unidentifiedAccessKey, - universalUnidentifiedAccess, - AppCapabilities.getCapabilities(isV2RegistrationLock)); - // TODO [greyson] [pins] ^^ This needs to be updated. It's not just for reglock, but also if they needed to enter a PIN at all + VerifyAccountResponse response = accountManager.verifyAccountWithCode(code, + null, + registrationId, + !hasFcm, + registrationLockV1, + registrationLockV2, + unidentifiedAccessKey, + universalUnidentifiedAccess, + AppCapabilities.getCapabilities(true)); + + UUID uuid = UuidUtil.parseOrThrow(response.getUuid()); + boolean hasPin = response.isStorageCapable(); IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); List records = PreKeyUtil.generatePreKeys(context); @@ -259,7 +263,7 @@ public final class CodeVerificationRequest { TextSecurePreferences.setPromptedPushRegistration(context, true); TextSecurePreferences.setUnauthorizedReceived(context, false); - PinState.onRegistration(context, kbsData, pin); + PinState.onRegistration(context, kbsData, pin, hasPin); } private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java index ffd7d813d1..4c0c52ba9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java @@ -12,7 +12,7 @@ public final class KeyBackupSystemWrongPinException extends Exception { this.tokenResponse = tokenResponse; } - @NonNull TokenResponse getTokenResponse() { + public @NonNull TokenResponse getTokenResponse() { return tokenResponse; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java index f1e5ac42bf..14028adaa0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java @@ -8,11 +8,13 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import java.util.HashSet; +import java.util.List; import java.util.Set; public final class StorageSyncValidations { @@ -20,15 +22,35 @@ public final class StorageSyncValidations { private StorageSyncValidations() {} public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) { - Set allSet = new HashSet<>(result.getManifest().getStorageIds()); - Set insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList()); + validateManifestAndInserts(result.getManifest(), result.getInserts()); + + if (result.getDeletes().size() > 0) { + Set allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet()); + + for (byte[] delete : result.getDeletes()) { + String encoded = Base64.encodeBytes(delete); + if (allSetEncoded.contains(encoded)) { + throw new DeletePresentInFullIdSetError(); + } + } + } + } + + + public static void validateForcePush(@NonNull SignalStorageManifest manifest, @NonNull List inserts) { + validateManifestAndInserts(manifest, inserts); + } + + private static void validateManifestAndInserts(@NonNull SignalStorageManifest manifest, @NonNull List inserts) { + Set allSet = new HashSet<>(manifest.getStorageIds()); + Set insertSet = new HashSet<>(Stream.of(inserts).map(SignalStorageRecord::getId).toList()); int accountCount = 0; - for (StorageId id : result.getManifest().getStorageIds()) { + for (StorageId id : manifest.getStorageIds()) { accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0; } - if (result.getInserts().size() > insertSet.size()) { + if (inserts.size() > insertSet.size()) { throw new DuplicateInsertInWriteError(); } @@ -40,7 +62,7 @@ public final class StorageSyncValidations { throw new MissingAccountError(); } - for (SignalStorageRecord insert : result.getInserts()) { + for (SignalStorageRecord insert : inserts) { if (!allSet.contains(insert.getId())) { throw new InsertNotPresentInFullIdSetError(); } @@ -57,17 +79,6 @@ public final class StorageSyncValidations { } } } - - if (result.getDeletes().size() > 0) { - Set allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet()); - - for (byte[] delete : result.getDeletes()) { - String encoded = Base64.encodeBytes(delete); - if (allSetEncoded.contains(encoded)) { - throw new DeletePresentInFullIdSetError(); - } - } - } } private static final class DuplicateInsertInWriteError extends Error { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 256eb22266..19ed656b43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -150,6 +150,17 @@ public class CommunicationActions { } } + public static void openEmail(@NonNull Context context, @NonNull String address, @Nullable String subject, @Nullable String body) { + Intent intent = new Intent(Intent.ACTION_SENDTO); + intent.setData(Uri.parse("mailto:")); + intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ address }); + intent.putExtra(Intent.EXTRA_SUBJECT, Util.emptyIfNull(subject)); + intent.putExtra(Intent.EXTRA_TEXT, Util.emptyIfNull(body)); + + context.startActivity(intent); + } + + private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) { try { Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index a72c479407..ec7f14f7e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -172,6 +172,10 @@ public class Util { return ""; } + public static @NonNull String emptyIfNull(@Nullable String value) { + return value != null ? value : ""; + } + public static List> chunk(@NonNull List list, int chunkSize) { List> chunks = new ArrayList<>(list.size() / chunkSize); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java index cf438a23aa..140ccdcbad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java @@ -38,6 +38,7 @@ import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; +import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; @@ -290,4 +291,9 @@ public class ViewUtil { } return result; } + + public static void hideKeyboard(@NonNull Context context, @NonNull View view) { + InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } } diff --git a/app/src/main/res/layout/pin_restore_activity.xml b/app/src/main/res/layout/pin_restore_activity.xml new file mode 100644 index 0000000000..ee75bfdb0f --- /dev/null +++ b/app/src/main/res/layout/pin_restore_activity.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pin_restore_entry_fragment.xml b/app/src/main/res/layout/pin_restore_entry_fragment.xml new file mode 100644 index 0000000000..a32ce5c663 --- /dev/null +++ b/app/src/main/res/layout/pin_restore_entry_fragment.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + +