Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.registration;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public final class PushChallengeRequest {
private static final String TAG = Log.tag(PushChallengeRequest.class);
/**
* Requests a push challenge and waits for the response.
* <p>
* Blocks the current thread for up to {@param timeoutMs} milliseconds.
*
* @param accountManager Account manager to request the push from.
* @param fcmToken Optional FCM token. If not present will return absent.
* @param e164number Local number.
* @param timeoutMs Timeout in milliseconds
* @return Either returns a challenge, or absent.
*/
@WorkerThread
public static Optional<String> getPushChallengeBlocking(@NonNull SignalServiceAccountManager accountManager,
@NonNull Optional<String> fcmToken,
@NonNull String e164number,
long timeoutMs)
{
if (!fcmToken.isPresent()) {
Log.w(TAG, "Push challenge not requested, as no FCM token was present");
return Optional.absent();
}
long startTime = System.currentTimeMillis();
Log.i(TAG, "Requesting a push challenge");
Request request = new Request(accountManager, fcmToken.get(), e164number, timeoutMs);
Optional<String> challenge = request.requestAndReceiveChallengeBlocking();
long duration = System.currentTimeMillis() - startTime;
if (challenge.isPresent()) {
Log.i(TAG, String.format(Locale.US, "Received a push challenge \"%s\" in %d ms", challenge.get(), duration));
} else {
Log.w(TAG, String.format(Locale.US, "Did not received a push challenge in %d ms", duration));
}
return challenge;
}
public static void postChallengeResponse(@NonNull String challenge) {
EventBus.getDefault().post(new PushChallengeEvent(challenge));
}
public static class Request {
private final CountDownLatch latch;
private final AtomicReference<String> challenge;
private final SignalServiceAccountManager accountManager;
private final String fcmToken;
private final String e164number;
private final long timeoutMs;
private Request(@NonNull SignalServiceAccountManager accountManager,
@NonNull String fcmToken,
@NonNull String e164number,
long timeoutMs)
{
this.latch = new CountDownLatch(1);
this.challenge = new AtomicReference<>();
this.accountManager = accountManager;
this.fcmToken = fcmToken;
this.e164number = e164number;
this.timeoutMs = timeoutMs;
}
@WorkerThread
private Optional<String> requestAndReceiveChallengeBlocking() {
EventBus eventBus = EventBus.getDefault();
eventBus.register(this);
try {
accountManager.requestPushChallenge(fcmToken, e164number);
latch.await(timeoutMs, TimeUnit.MILLISECONDS);
return Optional.fromNullable(challenge.get());
} catch (InterruptedException | IOException e) {
Log.w(TAG, "Error getting push challenge", e);
return Optional.absent();
} finally {
eventBus.unregister(this);
}
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onChallengeEvent(@NonNull PushChallengeEvent pushChallengeEvent) {
challenge.set(pushChallengeEvent.challenge);
latch.countDown();
}
}
static class PushChallengeEvent {
private final String challenge;
PushChallengeEvent(String challenge) {
this.challenge = challenge;
}
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.registration;
import androidx.annotation.NonNull;
public final class ReceivedSmsEvent {
private final @NonNull String code;
public ReceivedSmsEvent(@NonNull String code) {
this.code = code;
}
public @NonNull String getCode() {
return code;
}
}

View File

@@ -0,0 +1,101 @@
package org.thoughtcrime.securesms.registration;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.service.VerificationCodeParser;
import org.whispersystems.libsignal.util.guava.Optional;
public final class RegistrationNavigationActivity extends AppCompatActivity {
private static final String TAG = Log.tag(RegistrationNavigationActivity.class);
public static final String RE_REGISTRATION_EXTRA = "re_registration";
private SmsRetrieverReceiver smsRetrieverReceiver;
public static Intent newIntentForNewRegistration(@NonNull Context context) {
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
intent.putExtra(RE_REGISTRATION_EXTRA, false);
return intent;
}
public static Intent newIntentForReRegistration(@NonNull Context context) {
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
intent.putExtra(RE_REGISTRATION_EXTRA, true);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_registration_navigation);
initializeChallengeListener();
}
@Override
protected void onDestroy() {
super.onDestroy();
shutdownChallengeListener();
}
private void initializeChallengeListener() {
smsRetrieverReceiver = new SmsRetrieverReceiver();
registerReceiver(smsRetrieverReceiver, new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION));
}
private void shutdownChallengeListener() {
if (smsRetrieverReceiver != null) {
unregisterReceiver(smsRetrieverReceiver);
smsRetrieverReceiver = null;
}
}
private class SmsRetrieverReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "SmsRetrieverReceiver received a broadcast...");
if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
Bundle extras = intent.getExtras();
Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);
switch (status.getStatusCode()) {
case CommonStatusCodes.SUCCESS:
Optional<String> code = VerificationCodeParser.parse(context, (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE));
if (code.isPresent()) {
Log.i(TAG, "Received verification code.");
handleVerificationCodeReceived(code.get());
} else {
Log.w(TAG, "Could not parse verification code.");
}
break;
case CommonStatusCodes.TIMEOUT:
Log.w(TAG, "Hit a timeout waiting for the SMS to arrive.");
break;
}
} else {
Log.w(TAG, "SmsRetrieverReceiver received the wrong action?");
}
}
}
private void handleVerificationCodeReceived(@NonNull String code) {
EventBus.getDefault().post(new ReceivedSmsEvent(code));
}
}

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.SavedStateViewModelFactory;
import androidx.lifecycle.ViewModelProviders;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.LogSubmitActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA;
abstract class BaseRegistrationFragment extends Fragment {
private RegistrationViewModel model;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.model = getRegistrationViewModel(requireActivity());
}
protected @NonNull RegistrationViewModel getModel() {
return model;
}
protected boolean isReregister() {
Activity activity = getActivity();
if (activity == null) {
return false;
}
return activity.getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false);
}
protected static RegistrationViewModel getRegistrationViewModel(@NonNull FragmentActivity activity) {
SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity);
return ViewModelProviders.of(activity, savedStateViewModelFactory).get(RegistrationViewModel.class);
}
protected static void setSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setClickable(false);
button.setIndeterminateProgressMode(true);
button.setProgress(50);
}
}
protected static void cancelSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setProgress(0);
button.setIndeterminateProgressMode(false);
button.setClickable(true);
}
}
protected static void hideKeyboard(@NonNull Context context, @NonNull View view) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* Sets view up to allow log submitting after multiple taps.
*/
protected static void setDebugLogSubmitMultiTapView(@Nullable View view) {
if (view == null) return;
view.setOnClickListener(new View.OnClickListener() {
private static final int DEBUG_TAP_TARGET = 8;
private static final int DEBUG_TAP_ANNOUNCE = 4;
private int debugTapCounter;
@Override
public void onClick(View v) {
Context context = v.getContext();
debugTapCounter++;
if (debugTapCounter >= DEBUG_TAP_TARGET) {
context.startActivity(new Intent(context, LogSubmitActivity.class));
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
int remaining = DEBUG_TAP_TARGET - debugTapCounter;
Toast.makeText(context, context.getResources().getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show();
}
}
});
}
}

View File

@@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
/**
* Fragment that displays a Captcha in a WebView.
*/
public final class CaptchaFragment extends BaseRegistrationFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_captcha, container, false);
}
@Override
@SuppressLint("SetJavaScriptEnabled")
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
WebView webView = view.findViewById(R.id.registration_captcha_web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.clearCache(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME)) {
handleToken(url.substring(RegistrationConstants.SIGNAL_CAPTCHA_SCHEME.length()));
return true;
}
return false;
}
});
webView.loadUrl(RegistrationConstants.SIGNAL_CAPTCHA_URL);
}
private void handleToken(@NonNull String token) {
getModel().onCaptchaResponse(token);
Navigation.findNavController(requireView()).navigate(CaptchaFragmentDirections.actionCaptchaComplete());
}
}

View File

@@ -0,0 +1,106 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import java.util.ArrayList;
import java.util.Map;
public final class CountryPickerFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<Map<String, String>>> {
private EditText countryFilter;
private RegistrationViewModel model;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.fragment_registration_country_picker, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = BaseRegistrationFragment.getRegistrationViewModel(requireActivity());
countryFilter = view.findViewById(R.id.country_search);
countryFilter.addTextChangedListener(new FilterWatcher());
LoaderManager.getInstance(this).initLoader(0, null, this).forceLoad();
}
@Override
public void onListItemClick(@NonNull ListView listView, @NonNull View view, int position, long id) {
Map<String, String> item = (Map<String, String>) getListAdapter().getItem(position);
int countryCode = Integer.parseInt(item.get("country_code").replace("+", ""));
String countryName = item.get("country_name");
model.onCountrySelected(countryName, countryCode);
Navigation.findNavController(view).navigate(CountryPickerFragmentDirections.actionCountrySelected());
}
@Override
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int id, @Nullable Bundle args) {
return new CountryListLoader(getActivity());
}
@Override
public void onLoadFinished(@NonNull Loader<ArrayList<Map<String, String>>> loader,
@NonNull ArrayList<Map<String, String>> results)
{
String[] from = { "country_name", "country_code" };
int[] to = { R.id.country_name, R.id.country_code };
setListAdapter(new SimpleAdapter(getActivity(), results, R.layout.country_list_item, from, to));
applyFilter(countryFilter.getText());
}
private void applyFilter(@NonNull CharSequence text) {
SimpleAdapter listAdapter = (SimpleAdapter) getListAdapter();
if (listAdapter != null) {
listAdapter.getFilter().filter(text);
}
}
@Override
public void onLoaderReset(@NonNull Loader<ArrayList<Map<String, String>>> loader) {
setListAdapter(null);
}
private class FilterWatcher implements TextWatcher {
@Override
public void afterTextChanged(Editable s) {
applyFilter(s);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
}

View File

@@ -0,0 +1,306 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public final class EnterCodeFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(EnterCodeFragment.class);
private ScrollView scrollView;
private TextView header;
private VerificationCodeView verificationCodeView;
private VerificationPinKeyboard keyboard;
private CallMeCountDownView callMeCountDown;
private View wrongNumber;
private View noCodeReceivedHelp;
private boolean autoCompleting;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_enter_code, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
scrollView = view.findViewById(R.id.scroll_view);
header = view.findViewById(R.id.verify_header);
verificationCodeView = view.findViewById(R.id.code);
keyboard = view.findViewById(R.id.keyboard);
callMeCountDown = view.findViewById(R.id.call_me_count_down);
wrongNumber = view.findViewById(R.id.wrong_number);
noCodeReceivedHelp = view.findViewById(R.id.no_code);
connectKeyboard(verificationCodeView, keyboard);
setOnCodeFullyEnteredListener(verificationCodeView);
wrongNumber.setOnClickListener(v -> Navigation.findNavController(view).navigate(EnterCodeFragmentDirections.actionWrongNumber()));
callMeCountDown.setOnClickListener(v -> handlePhoneCallRequest());
callMeCountDown.setListener((v, remaining) -> {
if (remaining <= 30) {
scrollView.smoothScrollTo(0, v.getBottom());
callMeCountDown.setListener(null);
}
});
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
getModel().getSuccessfulCodeRequestAttempts().observe(this, (attempts) -> {
if (attempts >= 3) {
noCodeReceivedHelp.setVisibility(View.VISIBLE);
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
}
});
}
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
verificationCodeView.setOnCompleteListener(code -> {
RegistrationViewModel model = getModel();
model.onVerificationCodeEntered(code);
callMeCountDown.setVisibility(View.INVISIBLE);
wrongNumber.setVisibility(View.INVISIBLE);
keyboard.displayProgress();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, null,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
handleSuccessfulRegistration();
}
});
}
@Override
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
model.setStorageCredentials(storageCredentials);
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
}
});
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse triesRemaining) {
// Unexpected, because at this point, no pin has been provided by the user.
throw new AssertionError();
}
@Override
public void onTooManyAttempts() {
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
new AlertDialog.Builder(requireContext())
.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, (dialog, which) -> {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
})
.show();
}
});
}
@Override
public void onError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
});
});
}
private void handleSuccessfulRegistration() {
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onVerificationCodeReceived(@NonNull ReceivedSmsEvent event) {
verificationCodeView.clear();
List<Integer> parsedCode = convertVerificationCodeToDigits(event.getCode());
autoCompleting = true;
final int size = parsedCode.size();
for (int i = 0; i < size; i++) {
final int index = i;
verificationCodeView.postDelayed(() -> {
verificationCodeView.append(parsedCode.get(index));
if (index == size - 1) {
autoCompleting = false;
}
}, i * 200);
}
}
private static List<Integer> convertVerificationCodeToDigits(@Nullable String code) {
if (code == null || code.length() != 6) {
return Collections.emptyList();
}
List<Integer> result = new ArrayList<>(code.length());
try {
for (int i = 0; i < code.length(); i++) {
result.add(Integer.parseInt(Character.toString(code.charAt(i))));
}
} catch (NumberFormatException e) {
Log.w(TAG, "Failed to convert code into digits.", e);
return Collections.emptyList();
}
return result;
}
private void handlePhoneCallRequest() {
callMeCountDown.startCountDown(RegistrationConstants.SUBSEQUENT_CALL_AVAILABLE_AFTER);
RegistrationViewModel model = getModel();
String captcha = model.getCaptchaToken();
model.clearCaptchaResponse();
NavController navController = Navigation.findNavController(callMeCountDown);
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), RegistrationCodeRequest.Mode.PHONE_CALL, captcha,
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
@Override
public void onNeedCaptcha() {
navController.navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
}
@Override
public void requestSent(@Nullable String fcmToken) {
model.setFcmToken(fcmToken);
model.markASuccessfulAttempt();
}
@Override
public void onError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
}
});
}
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
keyboard.setOnKeyPressListener(key -> {
if (!autoCompleting) {
if (key >= 0) {
verificationCodeView.append(key);
} else {
verificationCodeView.delete();
}
}
});
}
@Override
public void onResume() {
super.onResume();
getModel().getLiveNumber().observe(this, (s) -> header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, s.getFullFormattedNumber())));
callMeCountDown.startCountDown(RegistrationConstants.FIRST_CALL_AVAILABLE_AFTER);
}
private void sendEmailToSupport() {
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ getString(R.string.RegistrationActivity_support_email) });
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.RegistrationActivity_code_support_subject));
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.RegistrationActivity_code_support_body,
getDevice(),
getAndroidVersion(),
BuildConfig.VERSION_NAME,
Locale.getDefault()));
startActivity(intent);
}
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);
}
}

View File

@@ -0,0 +1,420 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.Task;
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
private LabeledEditText countryCode;
private LabeledEditText number;
private ArrayAdapter<String> countrySpinnerAdapter;
private AsYouTypeFormatter countryFormatter;
private CircularProgressButton register;
private Spinner countrySpinner;
private View cancel;
private ScrollView scrollView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
countryCode = view.findViewById(R.id.country_code);
number = view.findViewById(R.id.number);
countrySpinner = view.findViewById(R.id.country_spinner);
cancel = view.findViewById(R.id.cancel_button);
scrollView = view.findViewById(R.id.scroll_view);
register = view.findViewById(R.id.registerButton);
initializeSpinner(countrySpinner);
setUpNumberInput();
register.setOnClickListener(v -> handleRegister(requireContext()));
if (isReregister()) {
cancel.setVisibility(View.VISIBLE);
cancel.setOnClickListener(v -> Navigation.findNavController(v).navigateUp());
} else {
cancel.setVisibility(View.GONE);
}
RegistrationViewModel model = getModel();
NumberViewState number = model.getNumber();
initNumber(number);
countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener());
if (model.hasCaptchaToken()) {
handleRegister(requireContext());
}
countryCode.getInput().setImeOptions(EditorInfo.IME_ACTION_NEXT);
}
private void setUpNumberInput() {
EditText numberInput = number.getInput();
numberInput.addTextChangedListener(new NumberChangedListener());
number.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, register.getBottom()), 250);
}
});
numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
numberInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
handleRegister(requireContext());
return true;
}
return false;
});
}
private void handleRegister(@NonNull Context context) {
if (TextUtils.isEmpty(countryCode.getText())) {
Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
return;
}
if (TextUtils.isEmpty(this.number.getText())) {
Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show();
return;
}
final NumberViewState number = getModel().getNumber();
final String e164number = number.getE164Number();
if (!number.isValid()) {
Dialogs.showAlertDialog(context,
getString(R.string.RegistrationActivity_invalid_number),
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number));
return;
}
PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context);
if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
handleRequestVerification(context, e164number, true);
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
handlePromptForNoPlayServices(context, e164number);
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
} else {
Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_play_services_error),
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
}
}
private void handleRequestVerification(@NonNull Context context, @NonNull String e164number, boolean fcmSupported) {
setSpinning(register);
disableAllEntries();
if (fcmSupported) {
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
task.addOnSuccessListener(none -> {
Log.i(TAG, "Successfully registered SMS listener.");
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_FCM_WITH_LISTENER);
});
task.addOnFailureListener(e -> {
Log.w(TAG, "Failed to register SMS listener.", e);
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_FCM_NO_LISTENER);
});
} else {
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_NO_FCM);
}
}
private void disableAllEntries() {
countryCode.setEnabled(false);
number.setEnabled(false);
countrySpinner.setEnabled(false);
cancel.setVisibility(View.GONE);
}
private void enableAllEntries() {
countryCode.setEnabled(true);
number.setEnabled(true);
countrySpinner.setEnabled(true);
if (isReregister()) {
cancel.setVisibility(View.VISIBLE);
}
}
private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) {
RegistrationViewModel model = getModel();
String captcha = model.getCaptchaToken();
model.clearCaptchaResponse();
NavController navController = Navigation.findNavController(register);
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
Log.i(TAG, "Local rate limited");
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
return;
}
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
@Override
public void onNeedCaptcha() {
if (getContext() == null) {
Log.i(TAG, "Got onNeedCaptcha response, but fragment is no longer attached.");
return;
}
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
@Override
public void requestSent(@Nullable String fcmToken) {
if (getContext() == null) {
Log.i(TAG, "Got requestSent response, but fragment is no longer attached.");
return;
}
model.setFcmToken(fcmToken);
model.markASuccessfulAttempt();
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis());
model.updateLimiter();
}
@Override
public void onError() {
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
});
}
private void initializeSpinner(Spinner countrySpinner) {
countrySpinnerAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item);
countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
setCountryDisplay(getString(R.string.RegistrationActivity_select_your_country));
countrySpinner.setAdapter(countrySpinnerAdapter);
countrySpinner.setOnTouchListener((view, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
pickCountry(view);
}
return true;
});
countrySpinner.setOnKeyListener((view, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
pickCountry(view);
return true;
}
return false;
});
}
private void pickCountry(@NonNull View view) {
Navigation.findNavController(view).navigate(R.id.action_pickCountry);
}
private void initNumber(@NonNull NumberViewState numberViewState) {
int countryCode = numberViewState.getCountryCode();
long number = numberViewState.getNationalNumber();
String regionDisplayName = numberViewState.getCountryDisplayName();
this.countryCode.setText(String.valueOf(countryCode));
setCountryDisplay(regionDisplayName);
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
setCountryFormatter(regionCode);
if (number != 0) {
this.number.setText(String.valueOf(number));
}
}
private void setCountryDisplay(String regionDisplayName) {
countrySpinnerAdapter.clear();
if (regionDisplayName == null) {
countrySpinnerAdapter.add(getString(R.string.RegistrationActivity_select_your_country));
} else {
countrySpinnerAdapter.add(regionDisplayName);
}
}
private class CountryCodeChangedListener implements TextWatcher {
@Override
public void afterTextChanged(Editable s) {
if (TextUtils.isEmpty(s) || !TextUtils.isDigitsOnly(s)) {
setCountryDisplay(null);
countryFormatter = null;
return;
}
int countryCode = Integer.parseInt(s.toString());
String regionCode = PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(countryCode);
setCountryFormatter(regionCode);
if (!TextUtils.isEmpty(regionCode) && !regionCode.equals("ZZ")) {
number.requestFocus();
int numberLength = number.getText().length();
number.getInput().setSelection(numberLength, numberLength);
}
RegistrationViewModel model = getModel();
model.onCountrySelected(null, countryCode);
setCountryDisplay(model.getNumber().getCountryDisplayName());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
private class NumberChangedListener implements TextWatcher {
@Override
public void afterTextChanged(Editable s) {
Long number = reformatText(s);
if (number == null) return;
RegistrationViewModel model = getModel();
model.setNationalNumber(number);
setCountryDisplay(model.getNumber().getCountryDisplayName());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
private Long reformatText(Editable s) {
if (countryFormatter == null) {
return null;
}
if (TextUtils.isEmpty(s)) {
return null;
}
countryFormatter.clear();
String formattedNumber = null;
StringBuilder justDigits = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
formattedNumber = countryFormatter.inputDigit(c);
justDigits.append(c);
}
}
if (formattedNumber != null && !s.toString().equals(formattedNumber)) {
s.replace(0, s.length(), formattedNumber);
}
if (justDigits.length() == 0) {
return null;
}
return Long.parseLong(justDigits.toString());
}
private void setCountryFormatter(@Nullable String regionCode) {
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
countryFormatter = regionCode != null ? util.getAsYouTypeFormatter(regionCode) : null;
reformatText(number.getText());
}
private void handlePromptForNoPlayServices(@NonNull Context context, @NonNull String e164number) {
new AlertDialog.Builder(context)
.setTitle(R.string.RegistrationActivity_missing_google_play_services)
.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, e164number, false))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}

View File

@@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Context;
import android.content.Intent;
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.FragmentActivity;
import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_blank, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentActivity activity = requireActivity();
if (!isReregister()) {
// TODO [greyson] Navigation
activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, MainActivity.class)));
}
activity.finish();
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
}
private static Intent getRoutedIntent(@NonNull Context context, Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(context, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
return intent;
}
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.registration.fragments;
final class RegistrationConstants {
private RegistrationConstants() {
}
static final int FIRST_CALL_AVAILABLE_AFTER = 64;
static final int SUBSEQUENT_CALL_AVAILABLE_AFTER = 300;
static final String TERMS_AND_CONDITIONS_URL = "https://signal.org/legal";
static final String SIGNAL_CAPTCHA_URL = "https://signalcaptchas.org/registration/generate.html";
static final String SIGNAL_CAPTCHA_SCHEME = "signalcaptcha://";
}

View File

@@ -0,0 +1,214 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public final class RegistrationLockFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(RegistrationLockFragment.class);
private EditText pinEntry;
private CircularProgressButton pinButton;
private long timeRemaining;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_lock, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
pinEntry = view.findViewById(R.id.pin);
pinButton = view.findViewById(R.id.pinButton);
View clarificationLabel = view.findViewById(R.id.clarification_label);
View subHeader = view.findViewById(R.id.verify_subheader);
View pinForgotButton = view.findViewById(R.id.forgot_button);
String code = getModel().getTextCodeEntered();
timeRemaining = RegistrationLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
pinEntry.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
boolean matchesTextCode = s != null && s.toString().equals(code);
clarificationLabel.setVisibility(matchesTextCode ? View.VISIBLE : View.INVISIBLE);
subHeader.setVisibility(matchesTextCode ? View.INVISIBLE : View.VISIBLE);
}
});
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
return false;
});
pinButton.setOnClickListener((v) -> {
hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
RegistrationViewModel model = getModel();
model.getTokenResponseCredentialsPair()
.observe(this, pair -> {
TokenResponse token = pair.first();
String credentials = pair.second();
updateContinueText(token, credentials);
});
model.onRegistrationLockFragmentCreate();
}
private void updateContinueText(@Nullable TokenResponse tokenResponse, @Nullable String storageCredentials) {
if (tokenResponse == null) {
if (storageCredentials == null) {
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue));
} else {
// TODO: This is the case where we can determine they are locked out
// no token, but do have storage credentials. Might want to change text.
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue));
}
} else {
int triesRemaining = tokenResponse.getTries();
if (triesRemaining == 1) {
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_last_attempt));
} else {
pinButton.setIdleText(getString(R.string.RegistrationActivity_continue_d_attempts_left, triesRemaining));
}
}
pinButton.setText(pinButton.getIdleText());
}
private void handlePinEntry() {
final String pin = pinEntry.getText().toString();
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
return;
}
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
String storageCredentials = model.getBasicStorageCredentials();
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
setSpinning(pinButton);
registrationService.verifyAccount(requireActivity(),
model.getFcmToken(),
model.getTextCodeEntered(),
pin, storageCredentials, tokenResponse,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
cancelSpinning(pinButton);
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
}
@Override
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
model.setStorageCredentials(storageCredentials);
cancelSpinning(pinButton);
pinEntry.setText("");
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_registration_lock_pin, Toast.LENGTH_LONG).show();
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
cancelSpinning(pinButton);
model.setKeyBackupCurrentToken(tokenResponse);
int triesRemaining = tokenResponse.getTries();
if (triesRemaining == 0) {
handleForgottenPin(timeRemaining);
return;
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_pin_incorrect)
.setMessage(getString(R.string.RegistrationActivity_you_have_d_tries_remaining, triesRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
@Override
public void onTooManyAttempts() {
cancelSpinning(pinButton);
new AlertDialog.Builder(requireContext())
.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();
}
@Override
public void onError() {
cancelSpinning(pinButton);
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
});
}
private void handleForgottenPin(long timeRemainingMs) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_oh_no)
.setMessage(getString(R.string.RegistrationActivity_registration_of_this_phone_number_will_be_possible_without_your_registration_lock_pin_after_seven_days_have_passed, (TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
}

View File

@@ -0,0 +1,340 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupBase;
import org.thoughtcrime.securesms.backup.FullBackupImporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.Locale;
public final class RestoreBackupFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(RestoreBackupFragment.class);
private TextView restoreBackupSize;
private TextView restoreBackupTime;
private TextView restoreBackupProgress;
private CircularProgressButton restoreButton;
private View skipRestoreButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_restore_backup, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
Log.i(TAG, "Backup restore.");
restoreBackupSize = view.findViewById(R.id.backup_size_text);
restoreBackupTime = view.findViewById(R.id.backup_created_text);
restoreBackupProgress = view.findViewById(R.id.backup_progress_text);
restoreButton = view.findViewById(R.id.restore_button);
skipRestoreButton = view.findViewById(R.id.skip_restore_button);
skipRestoreButton.setOnClickListener((v) -> {
Log.i(TAG, "User skipped backup restore.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionSkip());
});
if (isReregister()) {
Log.i(TAG, "Skipping backup restore during re-register.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
return;
}
if (TextSecurePreferences.isBackupEnabled(requireContext())) {
Log.i(TAG, "Backups enabled, so a backup must have been previously restored.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
return;
}
if (!Permissions.hasAll(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Log.i(TAG, "Skipping backup detection. We don't have the permission.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());
} else {
initializeBackupDetection(view);
}
}
@SuppressLint("StaticFieldLeak")
private void initializeBackupDetection(@NonNull View view) {
searchForBackup(backup -> {
Context context = getContext();
if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away.");
return;
}
if (backup == null) {
Log.i(TAG, "Skipping backup detection. No backup found, or permission revoked since.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionNoBackupFound());
} else {
restoreBackupSize.setText(getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backup.getSize())));
restoreBackupTime.setText(getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.US, backup.getTimestamp())));
restoreButton.setOnClickListener((v) -> handleRestore(v.getContext(), backup));
}
});
}
interface OnBackupSearchResultListener {
@MainThread
void run(@Nullable BackupUtil.BackupInfo backup);
}
static void searchForBackup(@NonNull OnBackupSearchResultListener listener) {
new AsyncTask<Void, Void, BackupUtil.BackupInfo>() {
@Override
protected @Nullable
BackupUtil.BackupInfo doInBackground(Void... voids) {
try {
return BackupUtil.getLatestBackup();
} catch (NoExternalStorageException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable BackupUtil.BackupInfo backup) {
listener.run(backup);
}
}.execute();
}
private void handleRestore(@NonNull Context context, @NonNull BackupUtil.BackupInfo backup) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
prompt.addTextChangedListener(new PassphraseAsYouTypeFormatter());
new AlertDialog.Builder(context)
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
.setView(view)
.setPositiveButton(R.string.RegistrationActivity_restore, (dialog, which) -> {
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(prompt.getWindowToken(), 0);
setSpinning(restoreButton);
skipRestoreButton.setVisibility(View.INVISIBLE);
String passphrase = prompt.getText().toString();
restoreAsynchronously(context, backup, passphrase);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
Log.i(TAG, "Prompt for backup passphrase shown to user.");
}
@SuppressLint("StaticFieldLeak")
private void restoreAsynchronously(@NonNull Context context,
@NonNull BackupUtil.BackupInfo backup,
@NonNull String passphrase)
{
new AsyncTask<Void, Void, BackupImportResult>() {
@Override
protected BackupImportResult doInBackground(Void... voids) {
try {
Log.i(TAG, "Starting backup restore.");
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backup.getFile(),
passphrase);
DatabaseFactory.upgradeRestored(context, database);
NotificationChannels.restoreContactNotificationChannels(context);
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context);
BackupPassphrase.set(context, passphrase);
TextSecurePreferences.setBackupEnabled(context, true);
LocalBackupListener.schedule(context);
Log.i(TAG, "Backup restore complete.");
return BackupImportResult.SUCCESS;
} catch (FullBackupImporter.DatabaseDowngradeException e) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e);
return BackupImportResult.FAILURE_VERSION_DOWNGRADE;
} catch (IOException e) {
Log.w(TAG, e);
return BackupImportResult.FAILURE_UNKNOWN;
}
}
@Override
protected void onPostExecute(@NonNull BackupImportResult result) {
cancelSpinning(restoreButton);
skipRestoreButton.setVisibility(View.VISIBLE);
restoreBackupProgress.setText("");
switch (result) {
case SUCCESS:
Log.i(TAG, "Successful backup restore.");
break;
case FAILURE_VERSION_DOWNGRADE:
Toast.makeText(context, R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show();
break;
case FAILURE_UNKNOWN:
Toast.makeText(context, R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show();
break;
}
}
}.execute();
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(@NonNull FullBackupBase.BackupEvent event) {
int count = event.getCount();
if (count == 0) {
restoreBackupProgress.setText(R.string.RegistrationActivity_checking);
} else {
restoreBackupProgress.setText(getString(R.string.RegistrationActivity_d_messages_so_far, count));
}
setSpinning(restoreButton);
skipRestoreButton.setVisibility(View.INVISIBLE);
if (event.getType() == FullBackupBase.BackupEvent.Type.FINISHED) {
Navigation.findNavController(requireView())
.navigate(RestoreBackupFragmentDirections.actionBackupRestored());
}
}
private enum BackupImportResult {
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_UNKNOWN
}
private static class PassphraseAsYouTypeFormatter implements TextWatcher {
private static final int GROUP_SIZE = 5;
@Override
public void afterTextChanged(Editable editable) {
removeSpans(editable);
addSpans(editable);
}
private void removeSpans(Editable editable) {
SpaceSpan[] paddingSpans = editable.getSpans(0, editable.length(), SpaceSpan.class);
for (SpaceSpan span : paddingSpans) {
editable.removeSpan(span);
}
}
private void addSpans(Editable editable) {
final int length = editable.length();
for (int i = GROUP_SIZE; i < length; i += GROUP_SIZE) {
editable.setSpan(new SpaceSpan(), i - 1, i, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (editable.length() > 30) {
editable.delete(30, editable.length());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
/**
* A {@link ReplacementSpan} adds a small space after a single character.
* Based on https://stackoverflow.com/a/51949578
*/
private static class SpaceSpan extends ReplacementSpan {
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return (int) (paint.measureText(text, start, end) * 1.7f);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
canvas.drawText(text.subSequence(start, end).toString(), x, y, paint);
}
}
}

View File

@@ -0,0 +1,152 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.navigation.ActivityNavigator;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
public final class WelcomeFragment extends BaseRegistrationFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
private CircularProgressButton continueButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(isReregister() ? R.layout.fragment_registration_blank
: R.layout.fragment_registration_welcome,
container,
false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (isReregister()) {
RegistrationViewModel model = getModel();
if (model.hasRestoreFlowBeenShown()) {
Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out");
if (!Navigation.findNavController(view).popBackStack()) {
FragmentActivity activity = requireActivity();
activity.finish();
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
}
return;
}
initializeNumber();
Log.i(TAG, "Skipping restore because this is a reregistration.");
model.setWelcomeSkippedOnRestore();
Navigation.findNavController(view)
.navigate(WelcomeFragmentDirections.actionSkipRestore());
} else {
setDebugLogSubmitMultiTapView(view.findViewById(R.id.image));
setDebugLogSubmitMultiTapView(view.findViewById(R.id.title));
continueButton = view.findViewById(R.id.welcome_continue_button);
continueButton.setOnClickListener(this::continueClicked);
view.findViewById(R.id.welcome_terms_button).setOnClickListener(v -> onTermsClicked());
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void continueClicked(@NonNull View view) {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE)
.ifNecessary()
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
.onAnyResult(() -> {
gatherInformationAndContinue(continueButton);
})
.execute();
}
private void gatherInformationAndContinue(@NonNull View view) {
setSpinning(continueButton);
RestoreBackupFragment.searchForBackup(backup -> {
Context context = getContext();
if (context == null) {
Log.i(TAG, "No context on fragment, must have navigated away.");
return;
}
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true);
initializeNumber();
cancelSpinning(continueButton);
if (backup == null) {
Log.i(TAG, "Skipping backup. No backup found, or no permission to look.");
Navigation.findNavController(view)
.navigate(WelcomeFragmentDirections.actionSkipRestore());
} else {
Navigation.findNavController(view)
.navigate(WelcomeFragmentDirections.actionRestore());
}
});
}
@SuppressLint("MissingPermission")
private void initializeNumber() {
Optional<Phonenumber.PhoneNumber> localNumber = Optional.absent();
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE)) {
localNumber = Util.getDeviceNumber(requireContext());
}
if (localNumber.isPresent()) {
getModel().onNumberDetected(localNumber.get().getCountryCode(), localNumber.get().getNationalNumber());
} else {
Optional<String> simCountryIso = Util.getSimCountryIso(requireContext());
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), 0);
}
}
}
private void onTermsClicked() {
CommunicationActions.openBrowserLink(requireContext(), RegistrationConstants.TERMS_AND_CONDITIONS_URL);
}
}

View File

@@ -0,0 +1,300 @@
package org.thoughtcrime.securesms.registration.service;
import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.registrationpin.InvalidPinException;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
public final class CodeVerificationRequest {
private static final String TAG = Log.tag(CodeVerificationRequest.class);
static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
if (basicStorageCredentials == null) return null;
if (!FeatureFlags.KBS) return null;
return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials);
}
private enum Result {
SUCCESS,
PIN_LOCKED,
KBS_WRONG_PIN,
RATE_LIMITED,
ERROR
}
/**
* Asynchronously verify the account via the code.
*
* @param fcmToken The FCM token for the device.
* @param code The code that was delivered to the user.
* @param pin The users registration pin.
* @param callback Exactly one method on this callback will be called.
* @param kbsTokenResponse By keeping the token, on failure, a newly returned token will be reused in subsequent pin
* attempts, preventing certain attacks, we can also track the attempts making missing replies easier to spot.
*/
static void verifyAccount(@NonNull Context context,
@NonNull Credentials credentials,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable String basicStorageCredentials,
@Nullable TokenResponse kbsTokenResponse,
@NonNull VerifyCallback callback)
{
new AsyncTask<Void, Void, Result>() {
private volatile LockedException lockedException;
private volatile KeyBackupSystemWrongPinException keyBackupSystemWrongPinException;
@Override
protected Result doInBackground(Void... voids) {
try {
verifyAccount(context, credentials, code, pin, basicStorageCredentials, kbsTokenResponse, fcmToken);
return Result.SUCCESS;
} catch (LockedException e) {
Log.w(TAG, e);
lockedException = e;
return Result.PIN_LOCKED;
} catch (RateLimitException e) {
Log.w(TAG, e);
return Result.RATE_LIMITED;
} catch (IOException e) {
Log.w(TAG, e);
return Result.ERROR;
} catch (KeyBackupSystemWrongPinException e) {
keyBackupSystemWrongPinException = e;
return Result.KBS_WRONG_PIN;
}
}
@Override
protected void onPostExecute(Result result) {
switch (result) {
case SUCCESS:
handleSuccessfulRegistration(context);
callback.onSuccessfulRegistration();
break;
case PIN_LOCKED:
callback.onIncorrectRegistrationLockPin(lockedException.getTimeRemaining(), lockedException.getBasicStorageCredentials());
break;
case RATE_LIMITED:
callback.onTooManyAttempts();
break;
case ERROR:
callback.onError();
break;
case KBS_WRONG_PIN:
callback.onIncorrectKbsRegistrationLockPin(keyBackupSystemWrongPinException.getTokenResponse());
break;
}
}
}.execute();
}
private static void handleSuccessfulRegistration(@NonNull Context context) {
JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.add(new DirectoryRefreshJob(false));
jobManager.add(new RotateCertificateJob(context));
DirectoryRefreshListener.schedule(context);
RotateSignedPreKeyListener.schedule(context);
}
private static void verifyAccount(@NonNull Context context,
@NonNull Credentials credentials,
@NonNull String code,
@Nullable String pin,
@Nullable String basicStorageCredentials,
@Nullable TokenResponse kbsTokenResponse,
@Nullable String fcmToken)
throws IOException, KeyBackupSystemWrongPinException
{
int registrationId = KeyHelper.generateRegistrationId(false);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
SessionUtil.archiveAllSessions(context);
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
RegistrationLockData kbsData = restoreMasterKey(pin, basicStorageCredentials, kbsTokenResponse);
String registrationLock = kbsData != null ? kbsData.getMasterKey().getRegistrationLock() : null;
boolean present = fcmToken != null;
UUID uuid = accountManager.verifyAccountWithCode(code, null, registrationId, !present,
pin, registrationLock,
unidentifiedAccessKey, universalUnidentifiedAccess);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword());
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (present) {
accountManager.setGcmId(Optional.fromNullable(fcmToken));
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientId selfId = recipientDatabase.getOrInsertFromE164(credentials.getE164number());
recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegistered(selfId, uuid);
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
TextSecurePreferences.setLocalUuid(context, uuid);
TextSecurePreferences.setFcmToken(context, fcmToken);
TextSecurePreferences.setFcmDisabled(context, !present);
TextSecurePreferences.setWebsocketRegistered(context, true);
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(Recipient.self().getId(),
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setVerifying(context, false);
TextSecurePreferences.setPushRegistered(context, true);
TextSecurePreferences.setPushServerPassword(context, credentials.getPassword());
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
TextSecurePreferences.setRegistrationLockMasterKey(context, kbsData, System.currentTimeMillis());
if (kbsData == null) {
//noinspection deprecation Only acceptable place to write the old pin.
TextSecurePreferences.setDeprecatedRegistrationLockPin(context, pin);
if (pin != null) {
if (FeatureFlags.KBS) {
Log.i(TAG, "Pin V1 successfully entered during registration, scheduling a migration to Pin V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
}
}
} else {
repostPinToResetTries(context, pin, kbsData);
}
TextSecurePreferences.setRegistrationLockEnabled(context, pin != null);
if (pin != null) {
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
}
}
private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) {
if (!FeatureFlags.KBS) return;
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
try {
RegistrationLockData newData = keyBackupService.newPinChangeSession(kbsData.getTokenResponse())
.setPin(pin);
TextSecurePreferences.setRegistrationLockMasterKey(context, newData, System.currentTimeMillis());
} catch (IOException e) {
Log.w(TAG, "May have failed to reset pin attempts!", e);
} catch (UnauthenticatedResponseException e) {
Log.w(TAG, "Failed to reset pin attempts", e);
} catch (InvalidPinException e) {
throw new AssertionError(e);
}
}
private static @Nullable RegistrationLockData restoreMasterKey(@Nullable String pin,
@Nullable String basicStorageCredentials,
@Nullable TokenResponse tokenResponse)
throws IOException, KeyBackupSystemWrongPinException
{
if (pin == null) return null;
if (basicStorageCredentials == null) {
Log.i(TAG, "No storage credentials supplied, pin is not on KBS");
return null;
}
if (!FeatureFlags.KBS) {
Log.w(TAG, "User appears to have a KBS pin, but this build has KBS off.");
return null;
}
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
Log.i(TAG, "Opening key backup service session");
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse);
try {
Log.i(TAG, "Restoring pin from KBS");
RegistrationLockData kbsData = session.restorePin(pin);
if (kbsData != null) {
Log.i(TAG, "Found registration lock token on KBS.");
} else {
Log.i(TAG, "No KBS data found.");
}
return kbsData;
} catch (UnauthenticatedResponseException e) {
Log.w(TAG, "Failed to restore key", e);
throw new IOException(e);
} catch (KeyBackupServicePinException e) {
Log.w(TAG, "Incorrect pin", e);
throw new KeyBackupSystemWrongPinException(e.getToken());
} catch (InvalidPinException e) {
Log.w(TAG, "Invalid pin", e);
return null;
}
}
public interface VerifyCallback {
void onSuccessfulRegistration();
/**
* @param timeRemaining Time until pin expires and number can be reused.
*/
void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials);
void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse kbsTokenResponse);
void onTooManyAttempts();
void onError();
}
}

View File

@@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.registration.service;
import androidx.annotation.NonNull;
public final class Credentials {
private final String e164number;
private final String password;
public Credentials(@NonNull String e164number, @NonNull String password) {
this.e164number = e164number;
this.password = password;
}
public @NonNull String getE164number() {
return e164number;
}
public @NonNull String getPassword() {
return password;
}
}

View File

@@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.registration.service;
import androidx.annotation.NonNull;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
final class KeyBackupSystemWrongPinException extends Exception {
private final TokenResponse tokenResponse;
KeyBackupSystemWrongPinException(@NonNull TokenResponse tokenResponse){
this.tokenResponse = tokenResponse;
}
@NonNull TokenResponse getTokenResponse() {
return tokenResponse;
}
}

View File

@@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.registration.service;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.gcm.FcmUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import java.io.IOException;
import java.util.Locale;
public final class RegistrationCodeRequest {
private static final long PUSH_REQUEST_TIMEOUT_MS = 5000L;
private static final String TAG = Log.tag(RegistrationCodeRequest.class);
/**
* Request a verification code to be sent according to the specified {@param mode}.
*
* The request will fire asynchronously, and exactly one of the methods on the {@param callback}
* will be called.
*/
@SuppressLint("StaticFieldLeak")
static void requestSmsVerificationCode(@NonNull Context context, @NonNull Credentials credentials, @Nullable String captchaToken, @NonNull Mode mode, @NonNull SmsVerificationCodeCallback callback) {
Log.d(TAG, String.format("SMS Verification requested for %s captcha %s", credentials.getE164number(), captchaToken));
new AsyncTask<Void, Void, VerificationRequestResult>() {
@Override
protected @NonNull
VerificationRequestResult doInBackground(Void... voids) {
try {
markAsVerifying(context);
Optional<String> fcmToken;
if (mode.isFcm()) {
fcmToken = FcmUtil.getToken();
} else {
fcmToken = Optional.absent();
}
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
Optional<String> pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, credentials.getE164number(), PUSH_REQUEST_TIMEOUT_MS);
if (mode == Mode.PHONE_CALL) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge);
} else {
accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported(), Optional.fromNullable(captchaToken), pushChallenge);
}
return new VerificationRequestResult(fcmToken.orNull(), Optional.absent());
} catch (IOException e) {
org.thoughtcrime.securesms.logging.Log.w(TAG, "Error during account registration", e);
return new VerificationRequestResult(null, Optional.of(e));
}
}
protected void onPostExecute(@NonNull VerificationRequestResult result) {
if (result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException) {
callback.onNeedCaptcha();
} else if (result.exception.isPresent()) {
callback.onError();
} else {
callback.requestSent(result.fcmToken);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static void markAsVerifying(Context context) {
TextSecurePreferences.setVerifying(context, true);
TextSecurePreferences.setPushRegistered(context, false);
}
private static class VerificationRequestResult {
private final @Nullable String fcmToken;
private final Optional<IOException> exception;
private VerificationRequestResult(@Nullable String fcmToken, Optional<IOException> exception) {
this.fcmToken = fcmToken;
this.exception = exception;
}
}
/**
* The mode by which a code is being requested.
*/
public enum Mode {
/**
* Device supports FCM and SMS retrieval.
*
* The SMS sent will be formatted for automatic SMS retrieval.
*/
SMS_FCM_WITH_LISTENER(true, true),
/**
* Device supports FCM but not SMS retrieval.
*
* The SMS sent will be not be specially formatted for automatic SMS retrieval.
*/
SMS_FCM_NO_LISTENER(true, false),
/**
* Device does not support FCM and so also not SMS retrieval.
*/
SMS_NO_FCM(false, false),
/**
* Device is requesting a phone call.
*
* Neither FCM or SMS retrieval is relevant in this mode.
*/
PHONE_CALL(false, false);
private final boolean fcm;
private final boolean smsRetrieverSupported;
Mode(boolean fcm, boolean smsRetrieverSupported) {
this.fcm = fcm;
this.smsRetrieverSupported = smsRetrieverSupported;
}
public boolean isFcm() {
return fcm;
}
public boolean isSmsRetrieverSupported() {
return smsRetrieverSupported;
}
}
public interface SmsVerificationCodeCallback {
void onNeedCaptcha();
void requestSent(@Nullable String fcmToken);
void onError();
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.registration.service;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
public final class RegistrationService {
private final Credentials credentials;
private RegistrationService(@NonNull Credentials credentials) {
this.credentials = credentials;
}
public static RegistrationService getInstance(@NonNull String e164number, @NonNull String password) {
return new RegistrationService(new Credentials(e164number, password));
}
/**
* See {@link RegistrationCodeRequest}.
*/
public void requestVerificationCode(@NonNull Activity activity,
@NonNull RegistrationCodeRequest.Mode mode,
@Nullable String captchaToken,
@NonNull RegistrationCodeRequest.SmsVerificationCodeCallback callback)
{
RegistrationCodeRequest.requestSmsVerificationCode(activity, credentials, captchaToken, mode, callback);
}
/**
* See {@link CodeVerificationRequest}.
*/
public void verifyAccount(@NonNull Activity activity,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable String basicStorageCredentials,
@Nullable TokenResponse tokenResponse,
@NonNull CodeVerificationRequest.VerifyCallback callback)
{
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, basicStorageCredentials, tokenResponse, callback);
}
public @Nullable TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
return CodeVerificationRequest.getToken(basicStorageCredentials);
}
}

View File

@@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class LocalCodeRequestRateLimiter implements Parcelable {
private final long timePeriod;
private final Map<RegistrationCodeRequest.Mode, Data> dataMap;
public LocalCodeRequestRateLimiter(long timePeriod) {
this.timePeriod = timePeriod;
this.dataMap = new HashMap<>();
}
@MainThread
public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
Data data = dataMap.get(mode);
return data == null || !data.limited(e164Number, currentTime);
}
/**
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
*/
@MainThread
public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
}
/**
* Call this if a mode was unsuccessful in sending.
*/
@MainThread
public void onUnsuccessfulRequest() {
dataMap.clear();
}
static class Data {
final String e164Number;
final long limitedUntil;
Data(@NonNull String e164Number, long limitedUntil) {
this.e164Number = e164Number;
this.limitedUntil = limitedUntil;
}
boolean limited(String e164Number, long currentTime) {
return this.e164Number.equals(e164Number) && currentTime < limitedUntil;
}
}
public static final Creator<LocalCodeRequestRateLimiter> CREATOR = new Creator<LocalCodeRequestRateLimiter>() {
@Override
public LocalCodeRequestRateLimiter createFromParcel(Parcel in) {
long timePeriod = in.readLong();
int numberOfMapEntries = in.readInt();
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
for (int i = 0; i < numberOfMapEntries; i++) {
RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()];
String e164Number = in.readString();
long limitedUntil = in.readLong();
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
}
return localCodeRequestRateLimiter;
}
@Override
public LocalCodeRequestRateLimiter[] newArray(int size) {
return new LocalCodeRequestRateLimiter[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(timePeriod);
dest.writeInt(dataMap.size());
for (Map.Entry<RegistrationCodeRequest.Mode, Data> a : dataMap.entrySet()) {
dest.writeInt(a.getKey().ordinal());
dest.writeString(a.getValue().e164Number);
dest.writeLong(a.getValue().limitedUntil);
}
}
}

View File

@@ -0,0 +1,186 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.util.Objects;
public final class NumberViewState implements Parcelable {
public static final NumberViewState INITIAL = new Builder().build();
private final String selectedCountryName;
private final int countryCode;
private final long nationalNumber;
private NumberViewState(Builder builder) {
this.selectedCountryName = builder.countryDisplayName;
this.countryCode = builder.countryCode;
this.nationalNumber = builder.nationalNumber;
}
public Builder toBuilder() {
return new Builder().countryCode(countryCode)
.selectedCountryDisplayName(selectedCountryName)
.nationalNumber(nationalNumber);
}
public int getCountryCode() {
return countryCode;
}
public long getNationalNumber() {
return nationalNumber;
}
public String getCountryDisplayName() {
if (selectedCountryName != null) {
return selectedCountryName;
}
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
if (isValid()) {
String actualCountry = getActualCountry(util, getE164Number());
if (actualCountry != null) {
return actualCountry;
}
}
String regionCode = util.getRegionCodeForCountryCode(countryCode);
return PhoneNumberFormatter.getRegionDisplayName(regionCode);
}
/**
* Finds actual name of region from a valid number. So for example +1 might map to US or Canada or other territories.
*/
private static @Nullable String getActualCountry(@NonNull PhoneNumberUtil util, @NonNull String e164Number) {
try {
Phonenumber.PhoneNumber phoneNumber = getPhoneNumber(util, e164Number);
String regionCode = util.getRegionCodeForNumber(phoneNumber);
if (regionCode != null) {
return PhoneNumberFormatter.getRegionDisplayName(regionCode);
}
} catch (NumberParseException e) {
return null;
}
return null;
}
public boolean isValid() {
return PhoneNumberFormatter.isValidNumber(getE164Number(), Integer.toString(getCountryCode()));
}
@Override
public int hashCode() {
int hash = countryCode;
hash *= 31;
hash += (int) (nationalNumber ^ (nationalNumber >>> 32));
hash *= 31;
hash += selectedCountryName != null ? selectedCountryName.hashCode() : 0;
return hash;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) return false;
if (obj.getClass() != getClass()) return false;
NumberViewState other = (NumberViewState) obj;
return other.countryCode == countryCode &&
other.nationalNumber == nationalNumber &&
Objects.equals(other.selectedCountryName, selectedCountryName);
}
public String getE164Number() {
return getConfiguredE164Number(countryCode, nationalNumber);
}
public String getFullFormattedNumber() {
return formatNumber(PhoneNumberUtil.getInstance(), getE164Number());
}
private static String formatNumber(@NonNull PhoneNumberUtil util, @NonNull String e164Number) {
try {
Phonenumber.PhoneNumber number = getPhoneNumber(util, e164Number);
return util.format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL);
} catch (NumberParseException e) {
return e164Number;
}
}
private static String getConfiguredE164Number(int countryCode, long number) {
return PhoneNumberFormatter.formatE164(String.valueOf(countryCode), String.valueOf(number));
}
private static Phonenumber.PhoneNumber getPhoneNumber(@NonNull PhoneNumberUtil util, @NonNull String e164Number)
throws NumberParseException
{
return util.parse(e164Number, null);
}
public static class Builder {
private String countryDisplayName;
private int countryCode;
private long nationalNumber;
public Builder countryCode(int countryCode) {
this.countryCode = countryCode;
return this;
}
public Builder selectedCountryDisplayName(String countryDisplayName) {
this.countryDisplayName = countryDisplayName;
return this;
}
public Builder nationalNumber(long nationalNumber) {
this.nationalNumber = nationalNumber;
return this;
}
public NumberViewState build() {
return new NumberViewState(this);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(selectedCountryName);
parcel.writeInt(countryCode);
parcel.writeLong(nationalNumber);
}
public static final Creator<NumberViewState> CREATOR = new Creator<NumberViewState>() {
@Override
public NumberViewState createFromParcel(Parcel in) {
return new Builder().selectedCountryDisplayName(in.readString())
.countryCode(in.readInt())
.nationalNumber(in.readLong())
.build();
}
@Override
public NumberViewState[] newArray(int size) {
return new NumberViewState[size];
}
};
}

View File

@@ -0,0 +1,204 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
public final class RegistrationViewModel extends ViewModel {
private static final String TAG = Log.tag(RegistrationViewModel.class);
private final String secret;
private final MutableLiveData<NumberViewState> number;
private final MutableLiveData<String> textCodeEntered;
private final MutableLiveData<String> captchaToken;
private final MutableLiveData<String> fcmToken;
private final MutableLiveData<String> basicStorageCredentials;
private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
private final MutableLiveData<String> keyBackupcurrentTokenJson;
private final LiveData<TokenResponse> keyBackupcurrentToken;
private final LiveData<Pair<TokenResponse, String>> tokenResponseCredentialsPair;
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL);
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
basicStorageCredentials = savedStateHandle.getLiveData("BASIC_STORAGE_CREDENTIALS");
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
keyBackupcurrentTokenJson = savedStateHandle.getLiveData("KBS_TOKEN");
keyBackupcurrentToken = Transformations.map(keyBackupcurrentTokenJson, json ->
{
if (json == null) return null;
try {
return JsonUtil.fromJson(json, TokenResponse.class);
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
});
tokenResponseCredentialsPair = new LiveDataPair<>(keyBackupcurrentToken, basicStorageCredentials);
}
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
if (!savedStateHandle.contains(key)) {
savedStateHandle.set(key, initialValue);
}
return savedStateHandle.get(key);
}
public @NonNull NumberViewState getNumber() {
//noinspection ConstantConditions Live data was given an initial value
return number.getValue();
}
public @NonNull LiveData<NumberViewState> getLiveNumber() {
return number;
}
public @NonNull String getTextCodeEntered() {
//noinspection ConstantConditions Live data was given an initial value
return textCodeEntered.getValue();
}
public String getCaptchaToken() {
return captchaToken.getValue();
}
public boolean hasCaptchaToken() {
return getCaptchaToken() != null;
}
public String getRegistrationSecret() {
return secret;
}
public void onCaptchaResponse(String captchaToken) {
this.captchaToken.setValue(captchaToken);
}
public void clearCaptchaResponse() {
captchaToken.setValue(null);
}
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
setViewState(getNumber().toBuilder()
.selectedCountryDisplayName(selectedCountryName)
.countryCode(countryCode).build());
}
public void setNationalNumber(long number) {
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
setViewState(numberViewState);
}
private void setViewState(NumberViewState numberViewState) {
if (!numberViewState.equals(getNumber())) {
number.setValue(numberViewState);
}
}
@MainThread
public void onVerificationCodeEntered(String code) {
textCodeEntered.setValue(code);
}
public void onNumberDetected(int countryCode, long nationalNumber) {
setViewState(getNumber().toBuilder()
.countryCode(countryCode)
.nationalNumber(nationalNumber)
.build());
}
public String getFcmToken() {
return fcmToken.getValue();
}
@MainThread
public void setFcmToken(@Nullable String fcmToken) {
this.fcmToken.setValue(fcmToken);
}
public void setWelcomeSkippedOnRestore() {
restoreFlowShown.setValue(true);
}
public boolean hasRestoreFlowBeenShown() {
//noinspection ConstantConditions Live data was given an initial value
return restoreFlowShown.getValue();
}
public void markASuccessfulAttempt() {
//noinspection ConstantConditions Live data was given an initial value
successfulCodeRequestAttempts.setValue(successfulCodeRequestAttempts.getValue() + 1);
}
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
return successfulCodeRequestAttempts;
}
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
//noinspection ConstantConditions Live data was given an initial value
return requestLimiter.getValue();
}
public void updateLimiter() {
requestLimiter.setValue(requestLimiter.getValue());
}
public void setStorageCredentials(@Nullable String storageCredentials) {
basicStorageCredentials.setValue(storageCredentials);
}
public @Nullable String getBasicStorageCredentials() {
return basicStorageCredentials.getValue();
}
public @Nullable TokenResponse getKeyBackupCurrentToken() {
return keyBackupcurrentToken.getValue();
}
public void setKeyBackupCurrentToken(TokenResponse tokenResponse) {
keyBackupcurrentTokenJson.setValue(tokenResponse == null ? null : JsonUtil.toJson(tokenResponse));
}
public LiveData<Pair<TokenResponse, String>> getTokenResponseCredentialsPair() {
return tokenResponseCredentialsPair;
}
public void onRegistrationLockFragmentCreate() {
SimpleTask.run(() -> {
RegistrationService registrationService = RegistrationService.getInstance(getNumber().getE164Number(), getRegistrationSecret());
try {
return registrationService.getToken(getBasicStorageCredentials());
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}, this::setKeyBackupCurrentToken);
}
}