Update registration to allow PIN entry.
This commit is contained in:
parent
6b37675a81
commit
acbfff89d3
46 changed files with 1206 additions and 161 deletions
|
@ -475,7 +475,12 @@
|
|||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
|
||||
FeatureFlags.update(config);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<SignalStorageManifest> 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
|
||||
|
|
|
@ -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<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
|
||||
long currentVersion = accountManager.getStorageManifestVersion();
|
||||
Map<RecipientId, StorageId> oldContactStorageIds = recipientDatabase.getContactStorageSyncIdsMap();
|
||||
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> 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<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> 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<StorageId> 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<RecipientId, StorageId> generateNewKeys(@NonNull Map<RecipientId, StorageId> oldKeys) {
|
||||
private static @NonNull Map<RecipientId, StorageId> generateContactStorageIds(@NonNull Map<RecipientId, StorageId> oldKeys) {
|
||||
Map<RecipientId, StorageId> out = new HashMap<>();
|
||||
|
||||
for (Map.Entry<RecipientId, StorageId> entry : oldKeys.entrySet()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<ConfirmKbsPinViewM
|
|||
public void onAnimationEnd(Animator animation) {
|
||||
requireActivity().setResult(Activity.RESULT_OK);
|
||||
closeNavGraphBranch();
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
@ -117,7 +120,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
|
|||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||
displayFailedDialog();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package org.thoughtcrime.securesms.logsubmit;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class LogSectionPin implements LogSection {
|
||||
|
||||
@Override
|
||||
public @NonNull String getTitle() {
|
||||
return "PIN STATE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CharSequence getContent(@NonNull Context context) {
|
||||
return new StringBuilder().append("State: ").append(SignalStore.pinValues().getPinState()).append("\n")
|
||||
.append("Last Successful Reminder Entry: ").append(SignalStore.pinValues().getLastSuccessfulEntryTime()).append("\n")
|
||||
.append("Next Reminder Interval: ").append(SignalStore.pinValues().getCurrentInterval()).append("\n")
|
||||
.append("ReglockV1: ").append(TextSecurePreferences.isV1RegistrationLockEnabled(context)).append("\n")
|
||||
.append("ReglockV2: ").append(SignalStore.kbsValues().isV2RegistrationLockEnabled()).append("\n")
|
||||
.append("Signal PIN: ").append(SignalStore.kbsValues().hasPin()).append("\n")
|
||||
.append("Needs Account Restore: ").append(SignalStore.storageServiceValues().needsAccountRestore()).append("\n")
|
||||
.append("PIN Required at Registration: ").append(SignalStore.registrationValues().pinWasRequiredAtRegistration()).append("\n")
|
||||
.append("Registration Complete: ").append(SignalStore.registrationValues().isRegistrationComplete());
|
||||
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ public class SubmitDebugLogRepository {
|
|||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(new LogSectionPower());
|
||||
}
|
||||
add(new LogSectionPin());
|
||||
add(new LogSectionThreads());
|
||||
add(new LogSectionFeatureFlags());
|
||||
add(new LogSectionPermissions());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<Optional<TokenData>> 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<PinResultData> 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<T> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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> triesRemaining;
|
||||
private final SingleLiveEvent<Event> 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<TriesRemaining> getTriesRemaining() {
|
||||
return triesRemaining;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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<PreKeyRecord> 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) {
|
||||
|
|
|
@ -12,7 +12,7 @@ public final class KeyBackupSystemWrongPinException extends Exception {
|
|||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
@NonNull TokenResponse getTokenResponse() {
|
||||
public @NonNull TokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<StorageId> allSet = new HashSet<>(result.getManifest().getStorageIds());
|
||||
Set<StorageId> insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList());
|
||||
validateManifestAndInserts(result.getManifest(), result.getInserts());
|
||||
|
||||
if (result.getDeletes().size() > 0) {
|
||||
Set<String> 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<SignalStorageRecord> inserts) {
|
||||
validateManifestAndInserts(manifest, inserts);
|
||||
}
|
||||
|
||||
private static void validateManifestAndInserts(@NonNull SignalStorageManifest manifest, @NonNull List<SignalStorageRecord> inserts) {
|
||||
Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
|
||||
Set<StorageId> 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<String> 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 {
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -172,6 +172,10 @@ public class Util {
|
|||
return "";
|
||||
}
|
||||
|
||||
public static @NonNull String emptyIfNull(@Nullable String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
|
||||
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
21
app/src/main/res/layout/pin_restore_activity.xml
Normal file
21
app/src/main/res/layout/pin_restore_activity.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".pin.PinRestoreActivity">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/pin_restore" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
114
app/src/main/res/layout/pin_restore_entry_fragment.xml
Normal file
114
app/src/main/res/layout/pin_restore_entry_fragment.xml
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_restore_pin_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/RegistrationLockFragment__enter_your_pin"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/pin_restore_keyboard_toggle"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_restore_pin_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:text="@string/RegistrationLockFragment__enter_the_pin_you_created"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_title" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/pin_restore_pin_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:inputType="numberPassword"
|
||||
android:minWidth="210dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_restore_pin_input_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_input"
|
||||
tools:text="@string/RegistrationLockFragment__incorrect_pin_try_again" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pin_restore_forgot_pin"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="@string/PinRestoreEntryFragment_need_help"
|
||||
android:textColor="@color/core_ultramarine"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_restore_pin_input_label"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/pin_restore_keyboard_toggle"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/pin_restore_pin_confirm"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_restore_forgot_pin"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:text="Create Alphanumeric Pin" />
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/pin_restore_pin_confirm"
|
||||
style="@style/Button.Registration"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cpb_textIdle="@string/RegistrationActivity_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pin_restore_skip_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/PinRestoreEntryFragment_skip"
|
||||
android:visibility="gone"
|
||||
style="@style/Button.Borderless.Registration"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
63
app/src/main/res/layout/pin_restore_locked_fragment.xml
Normal file
63
app/src/main/res/layout/pin_restore_locked_fragment.xml
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_locked_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="49dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/PinRestoreLockedFragment_create_your_pin"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin_locked_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
android:text="@string/PinRestoreLockedFragment_youve_run_out_of_pin_guesses"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pin_locked_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pin_locked_next"
|
||||
style="@style/Button.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/cta_button_background"
|
||||
android:text="@string/PinRestoreLockedFragment_create_new_pin"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toTopOf="@id/pin_locked_learn_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pin_locked_learn_more"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/AccountLockedFragment__learn_more"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
30
app/src/main/res/navigation/pin_restore.xml
Normal file
30
app/src/main/res/navigation/pin_restore.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/signup"
|
||||
app:startDestination="@id/pinEntryFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/pinEntryFragment"
|
||||
android:name="org.thoughtcrime.securesms.pin.PinRestoreEntryFragment"
|
||||
android:label="fragment_pin_entry"
|
||||
tools:layout="@layout/pin_restore_entry_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_accountLocked"
|
||||
app:destination="@id/pinLockedFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/pinLockedFragment"
|
||||
android:name="org.thoughtcrime.securesms.pin.PinRestoreLockedFragment"
|
||||
android:label="fragment_pin_locked"
|
||||
tools:layout="@layout/account_locked_fragment"/>
|
||||
|
||||
</navigation>
|
|
@ -132,6 +132,7 @@
|
|||
|
||||
<!-- CommunicationActions -->
|
||||
<string name="CommunicationActions_no_browser_found">No web browser found.</string>
|
||||
<string name="CommunicationActions_no_email_app_found">No email app found.</string>
|
||||
<string name="CommunicationActions_a_cellular_call_is_already_in_progress">A cellular call is already in progress.</string>
|
||||
<string name="CommunicationActions_start_video_call">Start video call?</string>
|
||||
<string name="CommunicationActions_start_voice_call">Start voice call?</string>
|
||||
|
@ -788,6 +789,32 @@
|
|||
<!-- PlayServicesProblemFragment -->
|
||||
<string name="PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning">The version of Google Play Services you have installed is not functioning correctly. Please reinstall Google Play Services and try again.</string>
|
||||
|
||||
<!-- PinRestoreEntryFragment -->
|
||||
<string name="PinRestoreEntryFragment_incorrect_pin">Incorrect PIN</string>
|
||||
<string name="PinRestoreEntryFragment_skip_pin_entry">Skip PIN entry?</string>
|
||||
<string name="PinRestoreEntryFragment_need_help">Need help?</string>
|
||||
<string name="PinRestoreEntryFragment_your_pin_is_a_d_digit_code">Your PIN is a %1$d+ digit code you created that can be numeric or alphanumeric.\n\nIf you can’t remember your PIN, you can create a new one. You can register and use your account but you’ll lose some saved settings like your profile information.</string>
|
||||
<string name="PinRestoreEntryFragment_if_you_cant_remember_your_pin">If you can’t remember your PIN, you can create a new one. You can register and use your account but you’ll lose some saved settings like your profile information.</string>
|
||||
<string name="PinRestoreEntryFragment_create_new_pin">Create New PIN</string>
|
||||
<string name="PinRestoreEntryFragment_contact_support">Contact Support</string>
|
||||
<string name="PinRestoreEntryFragment_cancel">Cancel</string>
|
||||
<string name="PinRestoreEntryFragment_skip">Skip</string>
|
||||
<plurals name="PinRestoreEntryFragment_you_have_d_attempt_remaining">
|
||||
<item quantity="one">You have %1$d attempt remaining. If you run out of attempts, you can create a new PIN. You can register and use your account but you’ll lose some saved settings like your profile information.</item>
|
||||
<item quantity="many">You have %1$d attempts remaining. If you run out of attempts, you can create a new PIN. You can register and use your account but you’ll lose some saved settings like your profile information.</item>
|
||||
</plurals>
|
||||
<string name="PinRestoreEntryFragment_support_email" translatable="false">support@signal.org</string>
|
||||
<string name="PinRestoreEntryFragment_signal_registration_need_help_with_pin">Signal Registration - Need Help with PIN for Android</string>
|
||||
<string name="PinRestoreEntryFragment_subject_signal_registration">Subject: Signal Registration - Need Help with PIN for Android\nDevice info: %1$s\nAndroid version: %2$s\nSignal version: %3$s\nLocale: %4$s</string>
|
||||
<string name="PinRestoreEntryFragment_enter_alphanumeric_pin">Enter alphanumeric PIN</string>
|
||||
<string name="PinRestoreEntryFragment_enter_numeric_pin">Enter numeric PIN</string>
|
||||
|
||||
<!-- PinRestoreLockedFragment -->
|
||||
<string name="PinRestoreLockedFragment_create_your_pin">Create your PIN</string>
|
||||
<string name="PinRestoreLockedFragment_youve_run_out_of_pin_guesses">You\'ve run out of PIN guesses, but you can still access your Signal account by creating a new PIN. For your privacy and security your account will be restored without any saved profile information or settings.</string>
|
||||
<string name="PinRestoreLockedFragment_create_new_pin">Create new PIN</string>
|
||||
<string name="PinRestoreLockedFragment_learn_more_url" translatable="false">https://support.signal.org/hc/articles/360007059792</string>
|
||||
|
||||
<!-- RatingManager -->
|
||||
<string name="RatingManager_rate_this_app">Rate this app</string>
|
||||
<string name="RatingManager_if_you_enjoy_using_this_app_please_take_a_moment">If you enjoy using this app, please take a moment to help us by rating it.</string>
|
||||
|
|
|
@ -102,7 +102,7 @@ public class PinsForAllScheduleTest {
|
|||
public void whenUserIsANewInstallAndFlagIsDisabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
|
||||
when(kbsValues.hasPin()).thenReturn(true);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
|
@ -116,7 +116,7 @@ public class PinsForAllScheduleTest {
|
|||
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
|
||||
when(kbsValues.hasPin()).thenReturn(true);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
|
|
|
@ -71,12 +71,19 @@ public final class KeyBackupService {
|
|||
/**
|
||||
* Only call before registration, to see how many tries are left.
|
||||
* <p>
|
||||
* Pass the token to the newRegistrationSession.
|
||||
* Pass the token to {@link #newRegistrationSession(String, TokenResponse)}.
|
||||
*/
|
||||
public TokenResponse getToken(String authAuthorization) throws IOException {
|
||||
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authorization token to be used with other requests.
|
||||
*/
|
||||
public String getAuthorization() throws IOException {
|
||||
return pushServiceSocket.getKeyBackupServiceAuthorization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
|
||||
*
|
||||
|
|
|
@ -62,6 +62,7 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
|||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||
|
@ -239,10 +240,10 @@ public class SignalServiceAccountManager {
|
|||
* @return The UUID of the user that was registered.
|
||||
* @throws IOException
|
||||
*/
|
||||
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||
SignalServiceProfile.Capabilities capabilities)
|
||||
public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||
SignalServiceProfile.Capabilities capabilities)
|
||||
throws IOException
|
||||
{
|
||||
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
|
||||
|
|
|
@ -271,23 +271,17 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public UUID verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
String pin, String registrationLock,
|
||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||
SignalServiceProfile.Capabilities capabilities)
|
||||
throws IOException
|
||||
{
|
||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
|
||||
String requestBody = JsonUtil.toJson(signalingKeyEntity);
|
||||
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
|
||||
VerifyAccountResponse response = JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
|
||||
Optional<UUID> uuid = UuidUtil.parse(response.getUuid());
|
||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
|
||||
String requestBody = JsonUtil.toJson(signalingKeyEntity);
|
||||
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
|
||||
|
||||
if (uuid.isPresent()) {
|
||||
return uuid.get();
|
||||
} else {
|
||||
throw new IOException("Invalid UUID!");
|
||||
}
|
||||
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
|
||||
}
|
||||
|
||||
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
|
|
|
@ -6,7 +6,14 @@ public class VerifyAccountResponse {
|
|||
@JsonProperty
|
||||
private String uuid;
|
||||
|
||||
@JsonProperty
|
||||
private boolean storageCapable;
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public boolean isStorageCapable() {
|
||||
return storageCapable;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue