Support for Registration Lock PINs

This commit is contained in:
Moxie Marlinspike
2018-03-01 09:51:37 -08:00
parent d28dc670ea
commit 110d33ddf8
22 changed files with 930 additions and 120 deletions

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -80,6 +81,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
initializeSearchListener();
RatingManager.showRatingDialogIfNecessary(this);
RegistrationLockDialog.showReminderIfNecessary(this);
}
@Override

View File

@@ -84,11 +84,14 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* The register account activity. Prompts ths user for their registration information
@@ -127,6 +130,12 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
private TextView restoreBackupProgress;
private CircularProgressButton restoreButton;
private View pinContainer;
private EditText pin;
private CircularProgressButton pinButton;
private TextView pinForgotButton;
private View pinClarificationContainer;
private CallMeCountDownView callMeCountDownView;
private VerificationPinKeyboard keyboard;
private VerificationCodeView verificationCodeView;
@@ -196,6 +205,12 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
this.restoreBackupProgress = findViewById(R.id.backup_progress_text);
this.restoreButton = findViewById(R.id.restore_button);
this.pinContainer = findViewById(R.id.pin_container);
this.pin = findViewById(R.id.pin);
this.pinButton = findViewById(R.id.pinButton);
this.pinForgotButton = findViewById(R.id.forgot_button);
this.pinClarificationContainer = findViewById(R.id.pin_clarification_container);
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
@@ -502,72 +517,36 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
callMeCountDownView.setVisibility(View.INVISIBLE);
keyboard.displayProgress();
new AsyncTask<Void, Void, Boolean>() {
new AsyncTask<Void, Void, Pair<Integer, Long>>() {
@Override
protected Boolean doInBackground(Void... voids) {
protected Pair<Integer, Long> doInBackground(Void... voids) {
try {
int registrationId = KeyHelper.generateRegistrationId(false);
TextSecurePreferences.setLocalRegistrationId(RegistrationActivity.this, registrationId);
SessionUtil.archiveAllSessions(RegistrationActivity.this);
String signalingKey = Util.getSecret(52);
accountManager.verifyAccountWithCode(code, signalingKey, registrationId, !registrationState.gcmToken.isPresent());
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(RegistrationActivity.this);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(RegistrationActivity.this);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(RegistrationActivity.this, identityKey, true);
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (registrationState.gcmToken.isPresent()) {
accountManager.setGcmId(registrationState.gcmToken);
}
TextSecurePreferences.setGcmRegistrationId(RegistrationActivity.this, registrationState.gcmToken.orNull());
TextSecurePreferences.setGcmDisabled(RegistrationActivity.this, !registrationState.gcmToken.isPresent());
TextSecurePreferences.setWebsocketRegistered(RegistrationActivity.this, true);
DatabaseFactory.getIdentityDatabase(RegistrationActivity.this)
.saveIdentity(Address.fromSerialized(registrationState.e164number),
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setVerifying(RegistrationActivity.this, false);
TextSecurePreferences.setPushRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setLocalNumber(RegistrationActivity.this, registrationState.e164number);
TextSecurePreferences.setPushServerPassword(RegistrationActivity.this, registrationState.password);
TextSecurePreferences.setSignalingKey(RegistrationActivity.this, signalingKey);
TextSecurePreferences.setSignedPreKeyRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
TextSecurePreferences.setUnauthorizedReceived(RegistrationActivity.this, false);
return true;
verifyAccount(code, null);
return new Pair<>(1, -1L);
} catch (LockedException e) {
Log.w(TAG, e);
return new Pair<>(2, e.getTimeRemaining());
} catch (IOException e) {
Log.w(TAG, e);
return false;
return new Pair<>(3, -1L);
}
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
protected void onPostExecute(Pair<Integer, Long> result) {
if (result.first == 1) {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new DirectoryRefreshJob(RegistrationActivity.this, false));
DirectoryRefreshListener.schedule(RegistrationActivity.this);
RotateSignedPreKeyListener.schedule(RegistrationActivity.this);
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
if (nextIntent == null) {
nextIntent = new Intent(RegistrationActivity.this, ConversationListActivity.class);
}
startActivity(nextIntent);
finish();
handleSuccessfulRegistration();
}
});
} else if (result.first == 2) {
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
registrationState = new RegistrationState(RegistrationState.State.PIN, registrationState);
displayPinView(code, result.second);
}
});
} else {
@@ -585,6 +564,67 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@SuppressLint("StaticFieldLeak")
private void handleVerifyWithPinClicked(@NonNull String code, @Nullable String pin) {
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
Toast.makeText(this, R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
return;
}
pinButton.setIndeterminateProgressMode(true);
pinButton.setProgress(50);
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... voids) {
try {
verifyAccount(code, pin);
return 1;
} catch (LockedException e) {
Log.w(TAG, e);
return 2;
} catch (RateLimitException e) {
Log.w(TAG, e);
return 3;
} catch (IOException e) {
Log.w(TAG, e);
return 4;
}
}
@Override
protected void onPostExecute(Integer result) {
pinButton.setIndeterminateProgressMode(false);
pinButton.setProgress(0);
if (result == 1) {
TextSecurePreferences.setRegistrationLockPin(RegistrationActivity.this, pin);
TextSecurePreferences.setRegistrationtLockEnabled(RegistrationActivity.this, true);
handleSuccessfulRegistration();
} else if (result == 2) {
RegistrationActivity.this.pin.setText("");
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_incorrect_registration_lock_pin, Toast.LENGTH_LONG).show();
} else if (result == 3) {
new AlertDialog.Builder(RegistrationActivity.this)
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
} else if (result == 4) {
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
}
}.execute();
}
private void handleForgottenPin(long timeRemaining) {
new AlertDialog.Builder(RegistrationActivity.this)
.setTitle(R.string.RegistrationActivity_oh_no)
.setMessage(getString(R.string.RegistrationActivity_registration_of_this_phone_number_will_be_possible_without_your_registration_lock_pin_after_seven_days_have_passed, (TimeUnit.MILLISECONDS.toDays(timeRemaining) + 1)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
@SuppressLint("StaticFieldLeak")
private void handlePhoneCallRequest() {
if (registrationState.state == RegistrationState.State.VERIFYING) {
@@ -605,6 +645,60 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
}
private void verifyAccount(@NonNull String code, @Nullable String pin) throws IOException {
int registrationId = KeyHelper.generateRegistrationId(false);
TextSecurePreferences.setLocalRegistrationId(RegistrationActivity.this, registrationId);
SessionUtil.archiveAllSessions(RegistrationActivity.this);
String signalingKey = Util.getSecret(52);
accountManager.verifyAccountWithCode(code, signalingKey, registrationId, !registrationState.gcmToken.isPresent(), pin);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(RegistrationActivity.this);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(RegistrationActivity.this);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(RegistrationActivity.this, identityKey, true);
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (registrationState.gcmToken.isPresent()) {
accountManager.setGcmId(registrationState.gcmToken);
}
TextSecurePreferences.setGcmRegistrationId(RegistrationActivity.this, registrationState.gcmToken.orNull());
TextSecurePreferences.setGcmDisabled(RegistrationActivity.this, !registrationState.gcmToken.isPresent());
TextSecurePreferences.setWebsocketRegistered(RegistrationActivity.this, true);
DatabaseFactory.getIdentityDatabase(RegistrationActivity.this)
.saveIdentity(Address.fromSerialized(registrationState.e164number),
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setVerifying(RegistrationActivity.this, false);
TextSecurePreferences.setPushRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setLocalNumber(RegistrationActivity.this, registrationState.e164number);
TextSecurePreferences.setPushServerPassword(RegistrationActivity.this, registrationState.password);
TextSecurePreferences.setSignalingKey(RegistrationActivity.this, signalingKey);
TextSecurePreferences.setSignedPreKeyRegistered(RegistrationActivity.this, true);
TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
TextSecurePreferences.setUnauthorizedReceived(RegistrationActivity.this, false);
}
private void handleSuccessfulRegistration() {
ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new DirectoryRefreshJob(RegistrationActivity.this, false));
DirectoryRefreshListener.schedule(RegistrationActivity.this);
RotateSignedPreKeyListener.schedule(RegistrationActivity.this);
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
if (nextIntent == null) {
nextIntent = new Intent(RegistrationActivity.this, ConversationListActivity.class);
}
startActivity(nextIntent);
finish();
}
private void displayRestoreView(@NonNull BackupUtil.BackupInfo backup) {
title.animate().translationX(title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
@Override
@@ -782,6 +876,64 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
this.callMeCountDownView.startCountDown(callCountdown);
}
private void displayPinView(String code, long lockedUntil) {
title.animate().translationX(-1 * title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
title.setText(R.string.RegistrationActivity_registration_lock_pin);
title.clearAnimation();
title.setTranslationX(title.getWidth());
title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
}
}).start();
subtitle.animate().translationX(-1 * subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
subtitle.setText(R.string.RegistrationActivity_this_phone_number_has_registration_lock_enabled_please_enter_the_registration_lock_pin);
subtitle.clearAnimation();
subtitle.setTranslationX(subtitle.getWidth());
subtitle.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
}
}).start();
verificationContainer.animate().translationX(-1 * verificationContainer.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
verificationContainer.clearAnimation();
verificationContainer.setVisibility(View.INVISIBLE);
verificationContainer.setTranslationX(0);
pinContainer.setTranslationX(pinContainer.getWidth());
pinContainer.setVisibility(View.VISIBLE);
pinContainer.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
}
}).start();
fab.animate().rotationBy(-360f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
fab.clearAnimation();
fab.setImageResource(R.drawable.ic_lock_white_24dp);
fab.animate().rotationBy(-360f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
}
}).start();
pinButton.setOnClickListener(v -> handleVerifyWithPinClicked(code, pin.getText().toString()));
pinForgotButton.setOnClickListener(v -> handleForgottenPin(lockedUntil));
pin.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (s != null && code.equals(s.toString())) pinClarificationContainer.setVisibility(View.VISIBLE);
else if (pinClarificationContainer.getVisibility() == View.VISIBLE) pinClarificationContainer.setVisibility(View.GONE);
}
});
}
private void handleCancel() {
TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
@@ -916,7 +1068,7 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
private static class RegistrationState {
private enum State {
INITIAL, VERIFYING, CHECKING
INITIAL, VERIFYING, CHECKING, PIN
}
private final State state;

View File

@@ -30,6 +30,7 @@ public class VerificationPinKeyboard extends FrameLayout {
private ProgressBar progressBar;
private ImageView successView;
private ImageView failureView;
private ImageView lockedView;
private OnKeyPressListener listener;
@@ -60,7 +61,8 @@ public class VerificationPinKeyboard extends FrameLayout {
this.keyboardView = findViewById(R.id.keyboard_view);
this.progressBar = findViewById(R.id.progress);
this.successView = findViewById(R.id.success);
this.failureView = findViewById(R.id.failure); ;
this.failureView = findViewById(R.id.failure);
this.lockedView = findViewById(R.id.locked);
keyboardView.setPreviewEnabled(false);
keyboardView.setKeyboard(new Keyboard(getContext(), R.xml.pin_keyboard));
@@ -97,6 +99,7 @@ public class VerificationPinKeyboard extends FrameLayout {
this.progressBar.setVisibility(View.GONE);
this.successView.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.lockedView.setVisibility(View.GONE);
}
public void displayProgress() {
@@ -104,6 +107,7 @@ public class VerificationPinKeyboard extends FrameLayout {
this.progressBar.setVisibility(View.VISIBLE);
this.successView.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.lockedView.setVisibility(View.GONE);
}
public ListenableFuture<Boolean> displaySuccess() {
@@ -112,6 +116,7 @@ public class VerificationPinKeyboard extends FrameLayout {
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.lockedView.setVisibility(View.GONE);
this.successView.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
@@ -143,6 +148,7 @@ public class VerificationPinKeyboard extends FrameLayout {
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.lockedView.setVisibility(View.GONE);
this.failureView.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
this.failureView.setVisibility(View.VISIBLE);
@@ -168,6 +174,38 @@ public class VerificationPinKeyboard extends FrameLayout {
return result;
}
public ListenableFuture<Boolean> displayLocked() {
SettableFuture<Boolean> result = new SettableFuture<>();
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.lockedView.setVisibility(View.GONE);
this.lockedView.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new OvershootInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
result.set(true);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.animateIn(this.lockedView, scaleAnimation);
return result;
}
public interface OnKeyPressListener {
void onKeyPress(int keyCode);
}

View File

@@ -33,6 +33,8 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.service.MessageRetrievalService;
@@ -75,7 +77,8 @@ import dagger.Provides;
RetrieveProfileAvatarJob.class,
MultiDeviceProfileKeyUpdateJob.class,
SendReadReceiptJob.class,
MultiDeviceReadReceiptUpdateJob.class})
MultiDeviceReadReceiptUpdateJob.class,
AppProtectionPreferenceFragment.class})
public class SignalCommunicationModule {
private static final String TAG = SignalCommunicationModule.class.getSimpleName();

View File

@@ -37,11 +37,12 @@ public class RefreshAttributesJob extends ContextJob implements InjectableType {
@Override
public void onRun() throws IOException {
String signalingKey = TextSecurePreferences.getSignalingKey(context);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean fetchesMessages = TextSecurePreferences.isGcmDisabled(context);
String signalingKey = TextSecurePreferences.getSignalingKey(context);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean fetchesMessages = TextSecurePreferences.isGcmDisabled(context);
String pin = TextSecurePreferences.getRegistrationLockPin(context);
signalAccountManager.setAccountAttributes(signalingKey, registrationId, fetchesMessages);
signalAccountManager.setAccountAttributes(signalingKey, registrationId, fetchesMessages, pin);
}
@Override

View File

@@ -0,0 +1,231 @@
package org.thoughtcrime.securesms.lock;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
public class RegistrationLockDialog {
private static final String TAG = RegistrationLockDialog.class.getSimpleName();
public static void showReminderIfNecessary(@NonNull Context context) {
if (!RegistrationLockReminders.needsReminder(context)) return;
AlertDialog dialog = new AlertDialog.Builder(context, R.style.RationaleDialog)
.setView(R.layout.registration_lock_reminder_view)
.setCancelable(true)
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
.create();
WindowManager windowManager = ServiceUtil.getWindowManager(context);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
dialog.getWindow().setLayout((int)(metrics.widthPixels * .75), ViewGroup.LayoutParams.WRAP_CONTENT);
dialog.show();
EditText pinEditText = dialog.findViewById(R.id.pin);
TextView reminder = dialog.findViewById(R.id.reminder);
assert pinEditText != null;
assert reminder != null;
SpannableString reminderText = new SpannableString(context.getString(R.string.RegistrationLockDialog_registration_lock_is_enabled_for_your_phone_number));
SpannableString forgotText = new SpannableString(context.getString(R.string.RegistrationLockDialog_i_forgot_my_pin));
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
dialog.dismiss();
new AlertDialog.Builder(context).setTitle(R.string.RegistrationLockDialog_forgotten_pin)
.setMessage(R.string.RegistrationLockDialog_registration_lock_helps_protect_your_phone_number_from_unauthorized_registration_attempts)
.setPositiveButton(android.R.string.ok, null)
.create()
.show();
}
};
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
reminder.setMovementMethod(LinkMovementMethod.getInstance());
pinEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (s != null && s.toString().replace(" ", "").equals(TextSecurePreferences.getRegistrationLockPin(context))) {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
}
}
});
}
@SuppressLint("StaticFieldLeak")
public static void showRegistrationLockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference, @NonNull SignalServiceAccountManager accountManager) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.RegistrationLockDialog_registration_lock)
.setView(R.layout.registration_lock_dialog_view)
.setPositiveButton(R.string.RegistrationLockDialog_enable, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(created -> {
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
EditText pin = dialog.findViewById(R.id.pin);
EditText repeat = dialog.findViewById(R.id.repeat);
ProgressBar progressBar = dialog.findViewById(R.id.progress);
assert pin != null;
assert repeat != null;
assert progressBar != null;
String pinValue = pin.getText().toString().replace(" ", "");
String repeatValue = repeat.getText().toString().replace(" ", "");
if (pinValue.length() < 4) {
Toast.makeText(context, R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_four_digits, Toast.LENGTH_LONG).show();
return;
}
if (!pinValue.equals(repeatValue)) {
Toast.makeText(context, R.string.RegistrationLockDialog_the_two_pins_you_entered_do_not_match, Toast.LENGTH_LONG).show();
return;
}
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
progressBar.setVisibility(View.VISIBLE);
progressBar.setIndeterminate(true);
button.setEnabled(false);
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
accountManager.setPin(Optional.of(pinValue));
TextSecurePreferences.setRegistrationLockPin(context, pinValue);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Override
protected void onPostExecute(@NonNull Boolean result) {
button.setEnabled(true);
progressBar.setVisibility(View.GONE);
if (result) {
preference.setChecked(true);
created.dismiss();
} else {
Toast.makeText(context, R.string.RegistrationLockDialog_error_connecting_to_the_service, Toast.LENGTH_LONG).show();
}
}
}.execute();
});
});
dialog.show();
}
@SuppressLint("StaticFieldLeak")
public static void showRegistrationUnlockPrompt(@NonNull Context context, @NonNull SwitchPreferenceCompat preference, @NonNull SignalServiceAccountManager accountManager) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.RegistrationLockDialog_disable_registration_lock_pin)
.setView(R.layout.registration_unlock_dialog_view)
.setPositiveButton(R.string.RegistrationLockDialog_disable, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(created -> {
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(v -> {
ProgressBar progressBar = dialog.findViewById(R.id.progress);
assert progressBar != null;
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
progressBar.setVisibility(View.VISIBLE);
progressBar.setIndeterminate(true);
button.setEnabled(false);
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
accountManager.setPin(Optional.absent());
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
progressBar.setVisibility(View.GONE);
button.setEnabled(true);
if (result) {
preference.setChecked(false);
created.dismiss();
} else {
Toast.makeText(context, R.string.RegistrationLockDialog_error_connecting_to_the_service, Toast.LENGTH_LONG).show();
}
}
}.execute();
});
});
dialog.show();
}
}

View File

@@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.lock;
import android.content.Context;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class RegistrationLockReminders {
public static final long INITIAL_INTERVAL = TimeUnit.HOURS.toMillis(6);
private static Map<Long, Long> INTERVAL_PROGRESSION = new HashMap<Long, Long>() {{
put(TimeUnit.HOURS.toMillis(6), TimeUnit.HOURS.toMillis(12));
put(TimeUnit.HOURS.toMillis(12), TimeUnit.DAYS.toMillis(1));
put(TimeUnit.DAYS.toMillis(1), TimeUnit.DAYS.toMillis(3));
put(TimeUnit.DAYS.toMillis(3), TimeUnit.DAYS.toMillis(7));
put(TimeUnit.DAYS.toMillis(7), TimeUnit.DAYS.toMillis(7));
}};
private static Map<Long, Long> INTERVAL_REGRESSION = new HashMap<Long, Long>() {{
put(TimeUnit.HOURS.toMillis(12), TimeUnit.HOURS.toMillis(6));
put(TimeUnit.DAYS.toMillis(1), TimeUnit.HOURS.toMillis(12));
put(TimeUnit.DAYS.toMillis(3), TimeUnit.DAYS.toMillis(1));
put(TimeUnit.DAYS.toMillis(7), TimeUnit.DAYS.toMillis(3));
}};
public static boolean needsReminder(@NonNull Context context) {
if (!TextSecurePreferences.isRegistrationtLockEnabled(context)) return false;
long lastReminderTime = TextSecurePreferences.getRegistrationLockLastReminderTime(context);
long nextIntervalTime = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
return System.currentTimeMillis() > lastReminderTime + nextIntervalTime;
}
public static void scheduleReminder(@NonNull Context context, boolean success) {
long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
long nextReminderInterval;
if (success) nextReminderInterval = INTERVAL_PROGRESSION.get(lastReminderInterval);
else nextReminderInterval = INTERVAL_REGRESSION.get(lastReminderInterval);
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.preferences;
import android.app.KeyguardManager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
@@ -12,8 +13,8 @@ import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.Preference;
import android.widget.Toast;
import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder;
import com.doomonafireball.betterpickers.hmspicker.HmsPickerDialogFragment;
import com.codetroopers.betterpickers.hmspicker.HmsPickerBuilder;
import com.codetroopers.betterpickers.hmspicker.HmsPickerDialogFragment;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
@@ -22,24 +23,39 @@ import org.thoughtcrime.securesms.PassphraseChangeActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadReceiptUpdateJob;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.util.concurrent.TimeUnit;
public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment {
import javax.inject.Inject;
public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment implements InjectableType {
private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked";
private CheckBoxPreference disablePassphrase;
@Inject
SignalServiceAccountManager accountManager;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
ApplicationContext.getInstance(activity).injectDependencies(this);
}
@Override
public void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF).setOnPreferenceClickListener(new AccountLockClickListener());
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
@@ -116,7 +132,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
}
private class ScreenLockTimeoutListener implements Preference.OnPreferenceClickListener, HmsPickerDialogFragment.HmsPickerDialogHandler {
private class ScreenLockTimeoutListener implements Preference.OnPreferenceClickListener, HmsPickerDialogFragment.HmsPickerDialogHandlerV2 {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -132,18 +148,31 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
return true;
}
@Override
public void onDialogHmsSet(int reference, int hours, int minutes, int seconds) {
public void onDialogHmsSet(int reference, boolean isNegative, int hours, int minutes, int seconds) {
long timeoutSeconds = Math.max(TimeUnit.HOURS.toSeconds(hours) +
TimeUnit.MINUTES.toSeconds(minutes) +
TimeUnit.SECONDS.toSeconds(seconds), 60);
TimeUnit.MINUTES.toSeconds(minutes) +
TimeUnit.SECONDS.toSeconds(seconds), 60);
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
initializeScreenLockTimeoutSummary();
}
}
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (((SwitchPreferenceCompat)preference).isChecked()) {
RegistrationLockDialog.showRegistrationUnlockPrompt(getContext(), (SwitchPreferenceCompat)preference, accountManager);
} else {
RegistrationLockDialog.showRegistrationLockPrompt(getContext(), (SwitchPreferenceCompat)preference, accountManager);
}
return true;
}
}
private class BlockedContactsClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -153,6 +182,40 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
}
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean enabled = (boolean)newValue;
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new MultiDeviceReadReceiptUpdateJob(getContext(), enabled));
return true;
}
}
public static CharSequence getSummary(Context context) {
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
if (TextSecurePreferences.isRegistrationtLockEnabled(context)) {
return context.getString(privacySummaryResId, offRes, onRes);
} else {
return context.getString(privacySummaryResId, offRes, offRes);
}
} else {
if (TextSecurePreferences.isRegistrationtLockEnabled(context)) {
return context.getString(privacySummaryResId, onRes, onRes);
} else {
return context.getString(privacySummaryResId, onRes, offRes);
}
}
}
// Derecated
private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -160,15 +223,15 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
startActivity(new Intent(getActivity(), PassphraseChangeActivity.class));
} else {
Toast.makeText(getActivity(),
R.string.ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet,
Toast.LENGTH_LONG).show();
R.string.ApplicationPreferenceActivity_you_havent_set_a_passphrase_yet,
Toast.LENGTH_LONG).show();
}
return true;
}
}
private class PassphraseIntervalClickListener implements Preference.OnPreferenceClickListener, HmsPickerDialogFragment.HmsPickerDialogHandler {
private class PassphraseIntervalClickListener implements Preference.OnPreferenceClickListener, HmsPickerDialogFragment.HmsPickerDialogHandlerV2 {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -186,12 +249,13 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
@Override
public void onDialogHmsSet(int reference, int hours, int minutes, int seconds) {
public void onDialogHmsSet(int reference, boolean isNegative, int hours, int minutes, int seconds) {
int timeoutMinutes = Math.max((int)TimeUnit.HOURS.toMinutes(hours) +
minutes +
(int)TimeUnit.SECONDS.toMinutes(seconds), 1);
minutes +
(int)TimeUnit.SECONDS.toMinutes(seconds), 1);
TextSecurePreferences.setPassphraseTimeoutInterval(getActivity(), timeoutMinutes);
initializePassphraseTimeoutSummary();
}
}
@@ -230,35 +294,4 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
}
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean enabled = (boolean)newValue;
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new MultiDeviceReadReceiptUpdateJob(getContext(), enabled));
return true;
}
}
public static CharSequence getSummary(Context context) {
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
if (TextSecurePreferences.isScreenSecurityEnabled(context)) {
return context.getString(privacySummaryResId, offRes, onRes);
} else {
return context.getString(privacySummaryResId, offRes, offRes);
}
} else {
if (TextSecurePreferences.isScreenSecurityEnabled(context)) {
return context.getString(privacySummaryResId, onRes, onRes);
} else {
return context.getString(privacySummaryResId, onRes, offRes);
}
}
}
}

View File

@@ -12,11 +12,11 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.util.Pair;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirementProvider;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.whispersystems.libsignal.util.Medium;
@@ -145,6 +145,11 @@ public class TextSecurePreferences {
public static final String SCREEN_LOCK = "pref_android_screen_lock";
public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout";
public static final String REGISTRATION_LOCK_PREF = "pref_registration_lock";
private static final String REGISTRATION_LOCK_PIN_PREF = "pref_registration_lock_pin";
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time";
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
public static boolean isScreenLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, SCREEN_LOCK, false);
}
@@ -161,6 +166,38 @@ public class TextSecurePreferences {
setLongPreference(context, SCREEN_LOCK_TIMEOUT, value);
}
public static boolean isRegistrationtLockEnabled(@NonNull Context context) {
return getBooleanPreference(context, REGISTRATION_LOCK_PREF, false);
}
public static void setRegistrationtLockEnabled(@NonNull Context context, boolean value) {
setBooleanPreference(context, REGISTRATION_LOCK_PREF, value);
}
public static @Nullable String getRegistrationLockPin(@NonNull Context context) {
return getStringPreference(context, REGISTRATION_LOCK_PIN_PREF, null);
}
public static void setRegistrationLockPin(@NonNull Context context, String pin) {
setStringPreference(context, REGISTRATION_LOCK_PIN_PREF, pin);
}
public static long getRegistrationLockLastReminderTime(@NonNull Context context) {
return getLongPreference(context, REGISTRATION_LOCK_LAST_REMINDER_TIME, 0);
}
public static void setRegistrationLockLastReminderTime(@NonNull Context context, long time) {
setLongPreference(context, REGISTRATION_LOCK_LAST_REMINDER_TIME, time);
}
public static long getRegistrationLockNextReminderInterval(@NonNull Context context) {
return getLongPreference(context, REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL, RegistrationLockReminders.INITIAL_INTERVAL);
}
public static void setRegistrationLockNextReminderInterval(@NonNull Context context, long value) {
setLongPreference(context, REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL, value);
}
public static void setBackupPassphrase(@NonNull Context context, @Nullable String passphrase) {
setStringPreference(context, BACKUP_PASSPHRASE, passphrase);
}