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:launchMode="singleTask"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
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:name=".service.ApplicationMigrationService"/>
|
||||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||||
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>
|
<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.logging.Log;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
|
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||||
|
@ -32,15 +33,17 @@ import java.util.Locale;
|
||||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
||||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
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_NORMAL = 0;
|
||||||
private static final int STATE_CREATE_PASSPHRASE = 1;
|
private static final int STATE_CREATE_PASSPHRASE = 1;
|
||||||
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
||||||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||||
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
|
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
|
||||||
private static final int STATE_CREATE_PROFILE_NAME = 5;
|
private static final int STATE_ENTER_SIGNAL_PIN = 5;
|
||||||
private static final int STATE_CREATE_KBS_PIN = 6;
|
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||||
|
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||||
|
|
||||||
private SignalServiceNetworkAccess networkAccess;
|
private SignalServiceNetworkAccess networkAccess;
|
||||||
private BroadcastReceiver clearKeyReceiver;
|
private BroadcastReceiver clearKeyReceiver;
|
||||||
|
@ -155,7 +158,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||||
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
||||||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
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();
|
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
@ -170,21 +174,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||||
return STATE_UI_BLOCKING_UPGRADE;
|
return STATE_UI_BLOCKING_UPGRADE;
|
||||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||||
return STATE_WELCOME_PUSH_SCREEN;
|
return STATE_WELCOME_PUSH_SCREEN;
|
||||||
|
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
|
||||||
|
return STATE_ENTER_SIGNAL_PIN;
|
||||||
} else if (userMustSetProfileName()) {
|
} else if (userMustSetProfileName()) {
|
||||||
return STATE_CREATE_PROFILE_NAME;
|
return STATE_CREATE_PROFILE_NAME;
|
||||||
} else if (userMustSetKbsPin()) {
|
} else if (userMustCreateSignalPin()) {
|
||||||
return STATE_CREATE_KBS_PIN;
|
return STATE_CREATE_SIGNAL_PIN;
|
||||||
} else {
|
} else {
|
||||||
return STATE_NORMAL;
|
return STATE_NORMAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean userMustSetKbsPin() {
|
private boolean userMustCreateSignalPin() {
|
||||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin();
|
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean userMustSetProfileName() {
|
private boolean userMustSetProfileName() {
|
||||||
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName() == ProfileName.EMPTY;
|
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getCreatePassphraseIntent() {
|
private Intent getCreatePassphraseIntent() {
|
||||||
|
@ -206,7 +212,11 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getCreateKbsPinIntent() {
|
private Intent getEnterSignalPinIntent() {
|
||||||
|
return getRoutedIntent(PinRestoreActivity.class, getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent getCreateSignalPinIntent() {
|
||||||
|
|
||||||
final Intent intent;
|
final Intent intent;
|
||||||
if (userMustSetProfileName()) {
|
if (userMustSetProfileName()) {
|
||||||
|
@ -252,4 +262,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||||
clearKeyReceiver = null;
|
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.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||||
|
|
||||||
|
@ -148,19 +149,10 @@ public class HelpFragment extends Fragment {
|
||||||
.map(view -> Feeling.getByViewId(view.getId()))
|
.map(view -> Feeling.getByViewId(view.getId()))
|
||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
|
|
||||||
Spanned body = getEmailBody(debugLog, feeling);
|
CommunicationActions.openEmail(requireContext(),
|
||||||
|
getString(R.string.RegistrationActivity_support_email),
|
||||||
Intent intent = new Intent(Intent.ACTION_SENDTO);
|
getEmailSubject(),
|
||||||
intent.setData(Uri.parse("mailto:"));
|
getEmailBody(debugLog, feeling).toString());
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getEmailSubject() {
|
private String getEmailSubject() {
|
||||||
|
|
|
@ -49,6 +49,11 @@ public class RefreshAttributesJob extends BaseJob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRun() throws IOException {
|
public void onRun() throws IOException {
|
||||||
|
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||||
|
Log.w(TAG, "Not yet registered. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
|
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
|
||||||
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
|
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
|
||||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
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.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -14,6 +16,8 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class RemoteConfigRefreshJob extends BaseJob {
|
public class RemoteConfigRefreshJob extends BaseJob {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(RemoteConfigRefreshJob.class);
|
||||||
|
|
||||||
public static final String KEY = "RemoteConfigRefreshJob";
|
public static final String KEY = "RemoteConfigRefreshJob";
|
||||||
|
|
||||||
public RemoteConfigRefreshJob() {
|
public RemoteConfigRefreshJob() {
|
||||||
|
@ -41,6 +45,11 @@ public class RemoteConfigRefreshJob extends BaseJob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRun() throws Exception {
|
protected void onRun() throws Exception {
|
||||||
|
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||||
|
Log.w(TAG, "Not registered. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
|
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
|
||||||
FeatureFlags.update(config);
|
FeatureFlags.update(config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,11 @@ public class RotateCertificateJob extends BaseJob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRun() throws IOException {
|
public void onRun() throws IOException {
|
||||||
|
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||||
|
Log.w(TAG, "Not yet registered. Ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (RotateCertificateJob.class) {
|
synchronized (RotateCertificateJob.class) {
|
||||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||||
byte[] certificate = accountManager.getSenderCertificate();
|
byte[] certificate = accountManager.getSenderCertificate();
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
@ -33,7 +34,7 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||||
|
|
||||||
public static String KEY = "StorageAccountRestoreJob";
|
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);
|
private static final String TAG = Log.tag(StorageAccountRestoreJob.class);
|
||||||
|
|
||||||
|
@ -69,7 +70,8 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||||
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
|
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
|
||||||
|
|
||||||
if (!manifest.isPresent()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,16 +99,13 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||||
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
|
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
|
||||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord);
|
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord);
|
||||||
|
|
||||||
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||||
|
|
||||||
if (accountRecord.getAvatarUrlPath().isPresent()) {
|
if (accountRecord.getAvatarUrlPath().isPresent()) {
|
||||||
RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get());
|
jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN/2);
|
||||||
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 RefreshAttributesJob(), LIFESPAN/2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
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.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
|
@ -78,20 +78,26 @@ public class StorageForcePushJob extends BaseJob {
|
||||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||||
|
|
||||||
long currentVersion = accountManager.getStorageManifestVersion();
|
long currentVersion = accountManager.getStorageManifestVersion();
|
||||||
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
|
Map<RecipientId, StorageId> oldContactStorageIds = recipientDatabase.getContactStorageSyncIdsMap();
|
||||||
|
|
||||||
long newVersion = currentVersion + 1;
|
long newVersion = currentVersion + 1;
|
||||||
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
|
Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
|
||||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
List<SignalStorageRecord> inserts = Stream.of(oldContactStorageIds.keySet())
|
||||||
.map(recipientDatabase::getRecipientSettings)
|
.map(recipientDatabase::getRecipientSettings)
|
||||||
.withoutNulls()
|
.withoutNulls()
|
||||||
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients))
|
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients))
|
||||||
.toList();
|
.toList();
|
||||||
inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId())));
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
if (newVersion > 1) {
|
if (newVersion > 1) {
|
||||||
|
@ -114,7 +120,8 @@ public class StorageForcePushJob extends BaseJob {
|
||||||
|
|
||||||
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
|
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
|
||||||
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
|
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
|
||||||
recipientDatabase.applyStorageIdUpdates(newStorageKeys);
|
recipientDatabase.applyStorageIdUpdates(newContactStorageIds);
|
||||||
|
recipientDatabase.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId()));
|
||||||
storageKeyDatabase.deleteAll();
|
storageKeyDatabase.deleteAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +134,7 @@ public class StorageForcePushJob extends BaseJob {
|
||||||
public void onFailure() {
|
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<>();
|
Map<RecipientId, StorageId> out = new HashMap<>();
|
||||||
|
|
||||||
for (Map.Entry<RecipientId, StorageId> entry : oldKeys.entrySet()) {
|
for (Map.Entry<RecipientId, StorageId> entry : oldKeys.entrySet()) {
|
||||||
|
|
|
@ -28,6 +28,8 @@ public final class KbsValues {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliberately does not clear the {@link #MASTER_KEY}.
|
* Deliberately does not clear the {@link #MASTER_KEY}.
|
||||||
|
*
|
||||||
|
* Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}
|
||||||
*/
|
*/
|
||||||
public void clearRegistrationLockAndPin() {
|
public void clearRegistrationLockAndPin() {
|
||||||
store.beginWrite()
|
store.beginWrite()
|
||||||
|
@ -37,6 +39,7 @@ public final class KbsValues {
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
|
||||||
public synchronized void setKbsMasterKey(@NonNull KbsPinData pinData, @NonNull String localPinHash) {
|
public synchronized void setKbsMasterKey(@NonNull KbsPinData pinData, @NonNull String localPinHash) {
|
||||||
MasterKey masterKey = pinData.getMasterKey();
|
MasterKey masterKey = pinData.getMasterKey();
|
||||||
String tokenResponse;
|
String tokenResponse;
|
||||||
|
@ -53,6 +56,7 @@ public final class KbsValues {
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Should only be set by {@link org.thoughtcrime.securesms.pin.PinState}. */
|
||||||
public synchronized void setV2RegistrationLockEnabled(boolean enabled) {
|
public synchronized void setV2RegistrationLockEnabled(boolean enabled) {
|
||||||
store.beginWrite().putBoolean(V2_LOCK_ENABLED, enabled).apply();
|
store.beginWrite().putBoolean(V2_LOCK_ENABLED, enabled).apply();
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ public final class PinValues {
|
||||||
return PinKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null));
|
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) {
|
public void setPinState(@NonNull String pinState) {
|
||||||
store.beginWrite().putString(PIN_STATE, pinState).commit();
|
store.beginWrite().putString(PIN_STATE, pinState).commit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ import java.security.SecureRandom;
|
||||||
|
|
||||||
public class StorageServiceValues {
|
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;
|
private final KeyValueStore store;
|
||||||
|
|
||||||
|
@ -29,4 +30,12 @@ public class StorageServiceValues {
|
||||||
public void onSyncCompleted() {
|
public void onSyncCompleted() {
|
||||||
store.beginWrite().putLong(LAST_SYNC_TIME, System.currentTimeMillis()).apply();
|
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) {
|
protected Boolean doInBackground(Void... voids) {
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "Setting pin on KBS - dialog");
|
Log.i(TAG, "Setting pin on KBS - dialog");
|
||||||
PinState.onCompleteRegistrationLockV1Reminder(context, pinValue);
|
PinState.onEnableLegacyRegistrationLockPreference(context, pinValue);
|
||||||
Log.i(TAG, "Pin set on KBS");
|
Log.i(TAG, "Pin set on KBS");
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException | UnauthenticatedResponseException e) {
|
} catch (IOException | UnauthenticatedResponseException e) {
|
||||||
|
@ -235,7 +235,7 @@ public final class RegistrationLockV1Dialog {
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Void... voids) {
|
protected Boolean doInBackground(Void... voids) {
|
||||||
try {
|
try {
|
||||||
PinState.onDisableRegistrationLockV1(context);
|
PinState.onDisableLegacyRegistrationLockPreference(context);
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException | UnauthenticatedResponseException e) {
|
} catch (IOException | UnauthenticatedResponseException e) {
|
||||||
Log.w(TAG, 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.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
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 org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -109,7 +111,8 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
|
||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
requireActivity().setResult(Activity.RESULT_OK);
|
requireActivity().setResult(Activity.RESULT_OK);
|
||||||
closeNavGraphBranch();
|
closeNavGraphBranch();
|
||||||
SignalStore.registrationValues().setRegistrationComplete();
|
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -117,7 +120,7 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewM
|
||||||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
|
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
SignalStore.registrationValues().setRegistrationComplete();
|
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||||
displayFailedDialog();
|
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) {
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
add(new LogSectionPower());
|
add(new LogSectionPower());
|
||||||
}
|
}
|
||||||
|
add(new LogSectionPin());
|
||||||
add(new LogSectionThreads());
|
add(new LogSectionThreads());
|
||||||
add(new LogSectionFeatureFlags());
|
add(new LogSectionFeatureFlags());
|
||||||
add(new LogSectionPermissions());
|
add(new LogSectionPermissions());
|
||||||
|
|
|
@ -42,7 +42,7 @@ class PinsForAllSchedule implements MegaphoneSchedule {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isEnabled() {
|
private static boolean isEnabled() {
|
||||||
if (SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
|
if (SignalStore.kbsValues().hasPin()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class PinsForAllSchedule implements MegaphoneSchedule {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newlyRegisteredV1PinUser()) {
|
if (newlyRegisteredRegistrationLockV1User()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,11 +67,11 @@ class PinsForAllSchedule implements MegaphoneSchedule {
|
||||||
|
|
||||||
private static boolean pinCreationFailedDuringRegistration() {
|
private static boolean pinCreationFailedDuringRegistration() {
|
||||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() &&
|
return SignalStore.registrationValues().pinWasRequiredAtRegistration() &&
|
||||||
!SignalStore.kbsValues().isV2RegistrationLockEnabled() &&
|
!SignalStore.kbsValues().hasPin() &&
|
||||||
!TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
|
!TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean newlyRegisteredV1PinUser() {
|
private static boolean newlyRegisteredRegistrationLockV1User() {
|
||||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() && TextSecurePreferences.isV1RegistrationLockEnabled(ApplicationDependencies.getApplication());
|
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.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.pin.PinState;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||||
|
@ -59,7 +60,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException, KeyBackupSystemNoDataException {
|
protected void onRun() throws IOException, UnauthenticatedResponseException {
|
||||||
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
|
if (!TextSecurePreferences.isV1RegistrationLockEnabled(context)) {
|
||||||
Log.i(TAG, "Registration lock disabled");
|
Log.i(TAG, "Registration lock disabled");
|
||||||
return;
|
return;
|
||||||
|
@ -74,19 +75,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Migrating pin to Key Backup Service");
|
Log.i(TAG, "Migrating pin to Key Backup Service");
|
||||||
|
PinState.onMigrateToRegistrationLockV2(context, pinValue);
|
||||||
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);
|
|
||||||
|
|
||||||
Log.i(TAG, "Pin migrated to Key Backup Service");
|
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 org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -84,35 +85,63 @@ public final class PinState {
|
||||||
*/
|
*/
|
||||||
public static synchronized void onRegistration(@NonNull Context context,
|
public static synchronized void onRegistration(@NonNull Context context,
|
||||||
@Nullable KbsPinData kbsData,
|
@Nullable KbsPinData kbsData,
|
||||||
@Nullable String pin)
|
@Nullable String pin,
|
||||||
|
boolean hasPinToRestore)
|
||||||
{
|
{
|
||||||
Log.i(TAG, "onNewRegistration()");
|
Log.i(TAG, "onNewRegistration()");
|
||||||
|
|
||||||
if (kbsData == null) {
|
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
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();
|
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());
|
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.
|
* 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
|
@WorkerThread
|
||||||
public static synchronized void onDisableRegistrationLockV1(@NonNull Context context)
|
public static synchronized void onDisableLegacyRegistrationLockPreference(@NonNull Context context)
|
||||||
throws IOException, UnauthenticatedResponseException
|
throws IOException, UnauthenticatedResponseException
|
||||||
{
|
{
|
||||||
Log.i(TAG, "onDisableRegistrationLockV1()");
|
Log.i(TAG, "onDisableRegistrationLockV1()");
|
||||||
|
@ -197,12 +227,16 @@ public final class PinState {
|
||||||
updateState(State.NO_REGISTRATION_LOCK);
|
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
|
@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
|
throws IOException, UnauthenticatedResponseException
|
||||||
{
|
{
|
||||||
Log.i(TAG, "onCompleteRegistrationLockV1Reminder()");
|
Log.i(TAG, "onCompleteRegistrationLockV1Reminder()");
|
||||||
assertState(State.REGISTRATION_LOCK_V1);
|
assertState(State.NO_REGISTRATION_LOCK);
|
||||||
|
|
||||||
KbsValues kbsValues = SignalStore.kbsValues();
|
KbsValues kbsValues = SignalStore.kbsValues();
|
||||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||||
|
@ -219,7 +253,29 @@ public final class PinState {
|
||||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
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() {
|
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() {
|
private static @NonNull State getState() {
|
||||||
|
@ -289,6 +345,7 @@ public final class PinState {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void updateState(@NonNull State state) {
|
private static void updateState(@NonNull State state) {
|
||||||
|
Log.i(TAG, "Updating state to: " + state);
|
||||||
SignalStore.pinValues().setPinState(state.serialize());
|
SignalStore.pinValues().setPinState(state.serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,8 @@ public class AvatarHelper {
|
||||||
* Whether or not an avatar is present for the given recipient.
|
* Whether or not an avatar is present for the given recipient.
|
||||||
*/
|
*/
|
||||||
public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) {
|
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());
|
this(in.readString(), in.readString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull
|
public @NonNull String getGivenName() {
|
||||||
String getGivenName() {
|
|
||||||
return givenName;
|
return givenName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.profiles.edit;
|
||||||
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.navigation.NavGraph;
|
import androidx.navigation.NavGraph;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
|
@ -23,6 +26,12 @@ public class EditProfileActivity extends BaseActionBarActivity implements EditPr
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
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
|
@Override
|
||||||
public void onCreate(Bundle bundle) {
|
public void onCreate(Bundle bundle) {
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
|
|
@ -45,6 +45,8 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
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.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||||
|
@ -307,9 +309,7 @@ public class EditProfileFragment extends Fragment {
|
||||||
private void handleUpload() {
|
private void handleUpload() {
|
||||||
viewModel.submitProfile(uploadResult -> {
|
viewModel.submitProfile(uploadResult -> {
|
||||||
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
|
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
|
||||||
if (SignalStore.kbsValues().hasPin()) {
|
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||||
SignalStore.registrationValues().setRegistrationComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PROFILE_NAMES_FOR_ALL);
|
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.RegistrationCodeRequest;
|
||||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||||
|
|
||||||
|
@ -305,16 +306,14 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendEmailToSupport() {
|
private void sendEmailToSupport() {
|
||||||
Intent intent = new Intent(Intent.ACTION_SENDTO);
|
CommunicationActions.openEmail(requireContext(),
|
||||||
intent.setData(Uri.parse("mailto:"));
|
getString(R.string.RegistrationActivity_support_email),
|
||||||
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ getString(R.string.RegistrationActivity_support_email) });
|
getString(R.string.RegistrationActivity_code_support_subject),
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.RegistrationActivity_code_support_subject));
|
getString(R.string.RegistrationActivity_code_support_body,
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.RegistrationActivity_code_support_body,
|
getDevice(),
|
||||||
getDevice(),
|
getAndroidVersion(),
|
||||||
getAndroidVersion(),
|
BuildConfig.VERSION_NAME,
|
||||||
BuildConfig.VERSION_NAME,
|
Locale.getDefault()));
|
||||||
Locale.getDefault()));
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getDevice() {
|
private static String getDevice() {
|
||||||
|
|
|
@ -13,7 +13,9 @@ import androidx.navigation.ActivityNavigator;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.MainActivity;
|
import org.thoughtcrime.securesms.MainActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||||
|
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
|
|
||||||
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
||||||
|
@ -30,12 +32,11 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
|
||||||
|
|
||||||
FragmentActivity activity = requireActivity();
|
FragmentActivity activity = requireActivity();
|
||||||
|
|
||||||
|
if (SignalStore.storageServiceValues().needsAccountRestore()) {
|
||||||
if (!isReregister()) {
|
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
|
||||||
|
} else if (!isReregister()) {
|
||||||
final Intent main = new Intent(activity, MainActivity.class);
|
final Intent main = new Intent(activity, MainActivity.class);
|
||||||
final Intent profile = new Intent(activity, EditProfileActivity.class);
|
final Intent profile = EditProfileActivity.getIntent(activity, false);
|
||||||
|
|
||||||
profile.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
|
|
||||||
|
|
||||||
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
|
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
|
||||||
activity.startActivity(chainIntents(chainIntents(profile, kbs), main));
|
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.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
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.contacts.entities.TokenResponse;
|
||||||
import org.whispersystems.signalservice.internal.push.LockedException;
|
import org.whispersystems.signalservice.internal.push.LockedException;
|
||||||
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
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));
|
Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2));
|
||||||
|
|
||||||
UUID uuid = accountManager.verifyAccountWithCode(code,
|
VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,
|
||||||
null,
|
null,
|
||||||
registrationId,
|
registrationId,
|
||||||
!hasFcm,
|
!hasFcm,
|
||||||
registrationLockV1,
|
registrationLockV1,
|
||||||
registrationLockV2,
|
registrationLockV2,
|
||||||
unidentifiedAccessKey,
|
unidentifiedAccessKey,
|
||||||
universalUnidentifiedAccess,
|
universalUnidentifiedAccess,
|
||||||
AppCapabilities.getCapabilities(isV2RegistrationLock));
|
AppCapabilities.getCapabilities(true));
|
||||||
// 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
|
|
||||||
|
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
|
||||||
|
boolean hasPin = response.isStorageCapable();
|
||||||
|
|
||||||
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
|
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||||
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
|
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
|
||||||
|
@ -259,7 +263,7 @@ public final class CodeVerificationRequest {
|
||||||
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
TextSecurePreferences.setPromptedPushRegistration(context, true);
|
||||||
TextSecurePreferences.setUnauthorizedReceived(context, false);
|
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) {
|
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ public final class KeyBackupSystemWrongPinException extends Exception {
|
||||||
this.tokenResponse = tokenResponse;
|
this.tokenResponse = tokenResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull TokenResponse getTokenResponse() {
|
public @NonNull TokenResponse getTokenResponse() {
|
||||||
return tokenResponse;
|
return tokenResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,13 @@ import com.annimon.stream.Stream;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
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.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public final class StorageSyncValidations {
|
public final class StorageSyncValidations {
|
||||||
|
@ -20,15 +22,35 @@ public final class StorageSyncValidations {
|
||||||
private StorageSyncValidations() {}
|
private StorageSyncValidations() {}
|
||||||
|
|
||||||
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) {
|
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) {
|
||||||
Set<StorageId> allSet = new HashSet<>(result.getManifest().getStorageIds());
|
validateManifestAndInserts(result.getManifest(), result.getInserts());
|
||||||
Set<StorageId> insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList());
|
|
||||||
|
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;
|
int accountCount = 0;
|
||||||
for (StorageId id : result.getManifest().getStorageIds()) {
|
for (StorageId id : manifest.getStorageIds()) {
|
||||||
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
|
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.getInserts().size() > insertSet.size()) {
|
if (inserts.size() > insertSet.size()) {
|
||||||
throw new DuplicateInsertInWriteError();
|
throw new DuplicateInsertInWriteError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +62,7 @@ public final class StorageSyncValidations {
|
||||||
throw new MissingAccountError();
|
throw new MissingAccountError();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SignalStorageRecord insert : result.getInserts()) {
|
for (SignalStorageRecord insert : inserts) {
|
||||||
if (!allSet.contains(insert.getId())) {
|
if (!allSet.contains(insert.getId())) {
|
||||||
throw new InsertNotPresentInFullIdSetError();
|
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 {
|
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) {
|
private static void startInsecureCallInternal(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||||
try {
|
try {
|
||||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress()));
|
Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + recipient.requireSmsAddress()));
|
||||||
|
|
|
@ -172,6 +172,10 @@ public class Util {
|
||||||
return "";
|
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) {
|
public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
|
||||||
List<List<E>> chunks = new ArrayList<>(list.size() / 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.ViewStub;
|
||||||
import android.view.animation.AlphaAnimation;
|
import android.view.animation.AlphaAnimation;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.LinearLayout.LayoutParams;
|
import android.widget.LinearLayout.LayoutParams;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -290,4 +291,9 @@ public class ViewUtil {
|
||||||
}
|
}
|
||||||
return result;
|
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 -->
|
<!-- CommunicationActions -->
|
||||||
<string name="CommunicationActions_no_browser_found">No web browser found.</string>
|
<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_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_video_call">Start video call?</string>
|
||||||
<string name="CommunicationActions_start_voice_call">Start voice call?</string>
|
<string name="CommunicationActions_start_voice_call">Start voice call?</string>
|
||||||
|
@ -788,6 +789,32 @@
|
||||||
<!-- PlayServicesProblemFragment -->
|
<!-- 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>
|
<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 -->
|
<!-- RatingManager -->
|
||||||
<string name="RatingManager_rate_this_app">Rate this app</string>
|
<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>
|
<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() {
|
public void whenUserIsANewInstallAndFlagIsDisabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||||
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
|
when(kbsValues.hasPin()).thenReturn(true);
|
||||||
when(FeatureFlags.pinsForAll()).thenReturn(false);
|
when(FeatureFlags.pinsForAll()).thenReturn(false);
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
|
@ -116,7 +116,7 @@ public class PinsForAllScheduleTest {
|
||||||
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||||
when(kbsValues.isV2RegistrationLockEnabled()).thenReturn(true);
|
when(kbsValues.hasPin()).thenReturn(true);
|
||||||
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
||||||
|
|
||||||
// WHEN
|
// WHEN
|
||||||
|
|
|
@ -71,12 +71,19 @@ public final class KeyBackupService {
|
||||||
/**
|
/**
|
||||||
* Only call before registration, to see how many tries are left.
|
* Only call before registration, to see how many tries are left.
|
||||||
* <p>
|
* <p>
|
||||||
* Pass the token to the newRegistrationSession.
|
* Pass the token to {@link #newRegistrationSession(String, TokenResponse)}.
|
||||||
*/
|
*/
|
||||||
public TokenResponse getToken(String authAuthorization) throws IOException {
|
public TokenResponse getToken(String authAuthorization) throws IOException {
|
||||||
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
|
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.
|
* 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.PushServiceSocket;
|
||||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
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.push.http.ProfileCipherOutputStreamFactory;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||||
|
@ -239,10 +240,10 @@ public class SignalServiceAccountManager {
|
||||||
* @return The UUID of the user that was registered.
|
* @return The UUID of the user that was registered.
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public UUID verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
|
||||||
String pin, String registrationLock,
|
String pin, String registrationLock,
|
||||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||||
SignalServiceProfile.Capabilities capabilities)
|
SignalServiceProfile.Capabilities capabilities)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
|
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,
|
String pin, String registrationLock,
|
||||||
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
|
||||||
SignalServiceProfile.Capabilities capabilities)
|
SignalServiceProfile.Capabilities capabilities)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
|
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities);
|
||||||
String requestBody = JsonUtil.toJson(signalingKeyEntity);
|
String requestBody = JsonUtil.toJson(signalingKeyEntity);
|
||||||
String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody);
|
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());
|
|
||||||
|
|
||||||
if (uuid.isPresent()) {
|
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
|
||||||
return uuid.get();
|
|
||||||
} else {
|
|
||||||
throw new IOException("Invalid UUID!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
|
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages,
|
||||||
|
|
|
@ -6,7 +6,14 @@ public class VerifyAccountResponse {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private String uuid;
|
private String uuid;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean storageCapable;
|
||||||
|
|
||||||
public String getUuid() {
|
public String getUuid() {
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isStorageCapable() {
|
||||||
|
return storageCapable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue