diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ec0cedd426..9d553cecfa 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -289,12 +289,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
-
-
@@ -400,10 +399,8 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable-hdpi/ic_action_name.png b/res/drawable-hdpi/ic_action_name.png
new file mode 100644
index 0000000000..798948971c
Binary files /dev/null and b/res/drawable-hdpi/ic_action_name.png differ
diff --git a/res/drawable-hdpi/ic_text_sms.png b/res/drawable-hdpi/ic_text_sms.png
new file mode 100644
index 0000000000..6a818fab6f
Binary files /dev/null and b/res/drawable-hdpi/ic_text_sms.png differ
diff --git a/res/drawable-mdpi/ic_action_name.png b/res/drawable-mdpi/ic_action_name.png
new file mode 100644
index 0000000000..eeefcc09d3
Binary files /dev/null and b/res/drawable-mdpi/ic_action_name.png differ
diff --git a/res/drawable-mdpi/ic_text_sms.png b/res/drawable-mdpi/ic_text_sms.png
new file mode 100644
index 0000000000..5c5a0f3cea
Binary files /dev/null and b/res/drawable-mdpi/ic_text_sms.png differ
diff --git a/res/drawable-xhdpi/ic_action_name.png b/res/drawable-xhdpi/ic_action_name.png
new file mode 100644
index 0000000000..28cd18b55b
Binary files /dev/null and b/res/drawable-xhdpi/ic_action_name.png differ
diff --git a/res/drawable-xhdpi/ic_text_sms.png b/res/drawable-xhdpi/ic_text_sms.png
new file mode 100644
index 0000000000..d95eb93192
Binary files /dev/null and b/res/drawable-xhdpi/ic_text_sms.png differ
diff --git a/res/drawable-xxhdpi/ic_action_name.png b/res/drawable-xxhdpi/ic_action_name.png
new file mode 100644
index 0000000000..c60efa5acb
Binary files /dev/null and b/res/drawable-xxhdpi/ic_action_name.png differ
diff --git a/res/drawable-xxhdpi/ic_text_sms.png b/res/drawable-xxhdpi/ic_text_sms.png
new file mode 100644
index 0000000000..1ce7442464
Binary files /dev/null and b/res/drawable-xxhdpi/ic_text_sms.png differ
diff --git a/res/layout/registration_activity.xml b/res/layout/registration_activity.xml
index fbee111f91..e34b96093d 100644
--- a/res/layout/registration_activity.xml
+++ b/res/layout/registration_activity.xml
@@ -1,137 +1,203 @@
-
-
-
+ android:layout_height="fill_parent"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:fillViewport="true"
+ android:background="@color/white"
+ tools:context=".RegistrationActivity">
-
+
-
+
-
+ android:textStyle="bold"
+ android:textColor="@color/white"
+ android:layout_gravity="center"
+ android:gravity="center"/>
-
-
-
+ android:paddingBottom="25dp"
+ android:textColor="@color/white"
+ android:text="@string/registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply"
+ android:gravity="center"/>
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+ android:layout_marginTop="20dp"
+ android:textColor="@color/gray50"
+ android:text="@android:string/cancel"/>
-
+ android:id="@+id/registration_information"
+ android:layout_width="fill_parent"
+ android:gravity="start"
+ android:visibility="gone"
+ android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
+ android:text="@string/registration_activity__registration_will_transmit_some_contact_information_to_the_server_temporariliy"/>
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
diff --git a/res/layout/registration_call_me_view.xml b/res/layout/registration_call_me_view.xml
new file mode 100644
index 0000000000..45c014f0e3
--- /dev/null
+++ b/res/layout/registration_call_me_view.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/verification_code_view.xml b/res/layout/verification_code_view.xml
new file mode 100644
index 0000000000..4c395cfedb
--- /dev/null
+++ b/res/layout/verification_code_view.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/verification_pin_keyboard_view.xml b/res/layout/verification_pin_keyboard_view.xml
new file mode 100644
index 0000000000..2cb45fa365
--- /dev/null
+++ b/res/layout/verification_pin_keyboard_view.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index b8f043cc45..fb7a552396 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -227,4 +227,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 781dc67653..7544d003cb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1488,6 +1488,8 @@
Optionally see and share when messages have been read
Enable read receipts
Shared media
+ Verify Your Number
+ Please enter your mobile number to receive a verification code. Carrier rates may apply.
diff --git a/res/xml/pin_keyboard.xml b/res/xml/pin_keyboard.xml
new file mode 100644
index 0000000000..0c1ad1c1f0
--- /dev/null
+++ b/res/xml/pin_keyboard.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/RegistrationActivity.java b/src/org/thoughtcrime/securesms/RegistrationActivity.java
index b30bafeed2..a1fbab50da 100644
--- a/src/org/thoughtcrime/securesms/RegistrationActivity.java
+++ b/src/org/thoughtcrime/securesms/RegistrationActivity.java
@@ -1,39 +1,79 @@
package org.thoughtcrime.securesms;
+import android.animation.Animator;
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.PorterDuff;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.os.AsyncTask;
import android.os.Bundle;
-import android.support.v4.content.ContextCompat;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
import android.util.Log;
+import android.util.Pair;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
+import android.view.animation.OvershootInterpolator;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
+import com.dd.CircularProgressButton;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
-import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
+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.crypto.IdentityKeyUtil;
+import org.thoughtcrime.securesms.crypto.PreKeyUtil;
+import org.thoughtcrime.securesms.crypto.SessionUtil;
+import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.IdentityDatabase;
+import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
+import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
+import org.thoughtcrime.securesms.push.AccountManagerFactory;
+import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
+import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.Dialogs;
+import org.thoughtcrime.securesms.util.PlayServicesUtil;
+import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus;
+import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
+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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
+import java.io.IOException;
+import java.util.List;
+
/**
* The register account activity. Prompts ths user for their registration information
* and begins the account registration process.
@@ -41,81 +81,106 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
* @author Moxie Marlinspike
*
*/
-public class RegistrationActivity extends BaseActionBarActivity {
+public class RegistrationActivity extends BaseActionBarActivity implements VerificationCodeView.OnCodeEnteredListener {
- private static final int PICK_COUNTRY = 1;
- private static final String TAG = RegistrationActivity.class.getSimpleName();
+ private static final int PICK_COUNTRY = 1;
+ private static final int SCENE_TRANSITION_DURATION = 250;
+ public static final String CHALLENGE_EVENT = "org.thoughtcrime.securesms.CHALLENGE_EVENT";
+ public static final String CHALLENGE_EXTRA = "CAAChallenge";
- private enum PlayServicesStatus {
- SUCCESS,
- MISSING,
- NEEDS_UPDATE,
- TRANSIENT_ERROR
- }
-
- private AsYouTypeFormatter countryFormatter;
- private ArrayAdapter countrySpinnerAdapter;
- private Spinner countrySpinner;
- private TextView countryCode;
- private TextView number;
- private TextView createButton;
- private TextView skipButton;
- private TextView informationView;
- private View informationToggle;
- private TextView informationToggleText;
+ private static final String TAG = RegistrationActivity.class.getSimpleName();
- private MasterSecret masterSecret;
+ private AsYouTypeFormatter countryFormatter;
+ private ArrayAdapter countrySpinnerAdapter;
+ private Spinner countrySpinner;
+ private TextView countryCode;
+ private TextView number;
+ private CircularProgressButton createButton;
+ private TextView informationView;
+ private TextView informationToggleText;
+ private TextView title;
+ private TextView subtitle;
+ private View registrationContainer;
+ private View verificationContainer;
+ private FloatingActionButton fab;
+
+ private CallMeCountDownView callMeCountDownView;
+ private VerificationPinKeyboard keyboard;
+ private VerificationCodeView verificationCodeView;
+ private RegistrationState registrationState;
+ private ChallengeReceiver challengeReceiver;
+ private SignalServiceAccountManager accountManager;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.registration_activity);
- getSupportActionBar().setTitle(getString(R.string.RegistrationActivity_connect_with_signal));
-
initializeResources();
initializeSpinner();
initializeNumber();
+ initializeChallengeListener();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ shutdownChallengeListener();
+ markAsVerifying(false);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PICK_COUNTRY && resultCode == RESULT_OK && data != null) {
- this.countryCode.setText(data.getIntExtra("country_code", 1)+"");
+ this.countryCode.setText(String.valueOf(data.getIntExtra("country_code", 1)));
setCountryDisplay(data.getStringExtra("country_name"));
setCountryFormatter(data.getIntExtra("country_code", 1));
}
}
private void initializeResources() {
- this.masterSecret = getIntent().getParcelableExtra("master_secret");
- this.countrySpinner = (Spinner) findViewById(R.id.country_spinner);
- this.countryCode = (TextView) findViewById(R.id.country_code);
- this.number = (TextView) findViewById(R.id.number);
- this.createButton = (TextView) findViewById(R.id.registerButton);
- this.skipButton = (TextView) findViewById(R.id.skipButton);
- this.informationView = (TextView) findViewById(R.id.registration_information);
- this.informationToggle = findViewById(R.id.information_link_container);
- this.informationToggleText = (TextView) findViewById(R.id.information_label);
-
- this.createButton.getBackground().setColorFilter(ContextCompat.getColor(this, R.color.signal_primary),
- PorterDuff.Mode.MULTIPLY);
- this.skipButton.getBackground().setColorFilter(ContextCompat.getColor(this, R.color.grey_400),
- PorterDuff.Mode.MULTIPLY);
+ TextView skipButton = findViewById(R.id.skip_button);
+ View informationToggle = findViewById(R.id.information_link_container);
+
+ this.countrySpinner = findViewById(R.id.country_spinner);
+ this.countryCode = findViewById(R.id.country_code);
+ this.number = findViewById(R.id.number);
+ this.createButton = findViewById(R.id.registerButton);
+ this.informationView = findViewById(R.id.registration_information);
+ this.informationToggleText = findViewById(R.id.information_label);
+ this.title = findViewById(R.id.verify_header);
+ this.subtitle = findViewById(R.id.verify_subheader);
+ this.registrationContainer = findViewById(R.id.registration_container);
+ this.verificationContainer = findViewById(R.id.verification_container);
+ this.fab = findViewById(R.id.fab);
+
+ this.verificationCodeView = findViewById(R.id.code);
+ this.keyboard = findViewById(R.id.keyboard);
+ this.callMeCountDownView = findViewById(R.id.call_me_count_down);
+ this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
this.number.addTextChangedListener(new NumberChangedListener());
- this.createButton.setOnClickListener(new CreateButtonListener());
- this.skipButton.setOnClickListener(new CancelButtonListener());
- this.informationToggle.setOnClickListener(new InformationToggleListener());
+ this.createButton.setOnClickListener(v -> handleRegister());
+ this.callMeCountDownView.setOnClickListener(v -> handlePhoneCallRequest());
+ skipButton.setOnClickListener(v -> handleCancel());
+ informationToggle.setOnClickListener(new InformationToggleListener());
if (getIntent().getBooleanExtra("cancel_button", false)) {
- this.skipButton.setVisibility(View.VISIBLE);
+ skipButton.setVisibility(View.VISIBLE);
} else {
- this.skipButton.setVisibility(View.INVISIBLE);
+ skipButton.setVisibility(View.INVISIBLE);
}
+
+ this.keyboard.setOnKeyPressListener(key -> {
+ if (key >= 0) verificationCodeView.append(key);
+ else verificationCodeView.delete();
+ });
+
+ this.verificationCodeView.setOnCompleteListener(this);
}
+ @SuppressLint("ClickableViewAccessibility")
private void initializeSpinner() {
this.countrySpinnerAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item);
this.countrySpinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@@ -123,26 +188,20 @@ public class RegistrationActivity extends BaseActionBarActivity {
setCountryDisplay(getString(R.string.RegistrationActivity_select_your_country));
this.countrySpinner.setAdapter(this.countrySpinnerAdapter);
- this.countrySpinner.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_UP) {
- Intent intent = new Intent(RegistrationActivity.this, CountrySelectionActivity.class);
- startActivityForResult(intent, PICK_COUNTRY);
- }
- return true;
+ this.countrySpinner.setOnTouchListener((v, event) -> {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ Intent intent = new Intent(RegistrationActivity.this, CountrySelectionActivity.class);
+ startActivityForResult(intent, PICK_COUNTRY);
}
+ return true;
});
- this.countrySpinner.setOnKeyListener(new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
- Intent intent = new Intent(RegistrationActivity.this, CountrySelectionActivity.class);
- startActivityForResult(intent, PICK_COUNTRY);
- return true;
- }
- return false;
+ this.countrySpinner.setOnKeyListener((v, keyCode, event) -> {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && event.getAction() == KeyEvent.ACTION_UP) {
+ Intent intent = new Intent(RegistrationActivity.this, CountrySelectionActivity.class);
+ startActivityForResult(intent, PICK_COUNTRY);
+ return true;
}
+ return false;
});
}
@@ -156,7 +215,7 @@ public class RegistrationActivity extends BaseActionBarActivity {
Optional simCountryIso = Util.getSimCountryIso(this);
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
- this.countryCode.setText(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get())+"");
+ this.countryCode.setText(String.valueOf(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get())));
}
}
}
@@ -179,119 +238,370 @@ public class RegistrationActivity extends BaseActionBarActivity {
number.getText().toString());
}
- private class CreateButtonListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- final RegistrationActivity self = RegistrationActivity.this;
+ private void handleRegister() {
+ if (TextUtils.isEmpty(countryCode.getText())) {
+ Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
+ return;
+ }
- if (TextUtils.isEmpty(countryCode.getText())) {
- Toast.makeText(self,
- getString(R.string.RegistrationActivity_you_must_specify_your_country_code),
- Toast.LENGTH_LONG).show();
- return;
- }
+ if (TextUtils.isEmpty(number.getText())) {
+ Toast.makeText(this, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show();
+ return;
+ }
- if (TextUtils.isEmpty(number.getText())) {
- Toast.makeText(self,
- getString(R.string.RegistrationActivity_you_must_specify_your_phone_number),
- Toast.LENGTH_LONG).show();
- return;
+ final String e164number = getConfiguredE164Number();
+
+ if (!PhoneNumberFormatter.isValidNumber(e164number)) {
+ Dialogs.showAlertDialog(this, getString(R.string.RegistrationActivity_invalid_number),
+ String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
+ e164number));
+ return;
+ }
+
+ PlayServicesStatus gcmStatus = PlayServicesUtil.getPlayServicesStatus(this);
+
+ if (gcmStatus == PlayServicesStatus.SUCCESS) {
+ handleRequestVerification(e164number, true);
+ } else if (gcmStatus == PlayServicesStatus.MISSING) {
+ handlePromptForNoPlayServices(e164number);
+ } else if (gcmStatus == PlayServicesStatus.NEEDS_UPDATE) {
+ GoogleApiAvailability.getInstance().getErrorDialog(this, ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
+ } else {
+ Dialogs.showAlertDialog(this, getString(R.string.RegistrationActivity_play_services_error),
+ getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private void handleRequestVerification(@NonNull String e164number, boolean gcmSupported) {
+ createButton.setIndeterminateProgressMode(true);
+ createButton.setProgress(50);
+
+ new AsyncTask>> () {
+ @Override
+ protected @Nullable Pair> doInBackground(Void... voids) {
+ try {
+ markAsVerifying(true);
+
+ String password = Util.getSecret(18);
+
+ Optional gcmToken;
+
+ if (gcmSupported) {
+ gcmToken = Optional.of(GoogleCloudMessaging.getInstance(RegistrationActivity.this).register(GcmRefreshJob.REGISTRATION_ID));
+ } else {
+ gcmToken = Optional.absent();
+ }
+
+ accountManager = AccountManagerFactory.createManager(RegistrationActivity.this, e164number, password);
+ accountManager.requestSmsVerificationCode();
+
+ return new Pair<>(password, gcmToken);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
}
- final String e164number = getConfiguredE164Number();
+ protected void onPostExecute(@Nullable Pair> result) {
+ if (result == null) {
+ Toast.makeText(RegistrationActivity.this, "Unable to connect to service. Please check network connection and try again.", Toast.LENGTH_LONG).show();
+ return;
+ }
- if (!PhoneNumberFormatter.isValidNumber(e164number)) {
- Dialogs.showAlertDialog(self,
- getString(R.string.RegistrationActivity_invalid_number),
- String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
- e164number));
- return;
+ registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.first, result.second);
+ displayVerificationView(e164number, 64);
}
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
- PlayServicesStatus gcmStatus = checkPlayServices(self);
+ private void handleChallengeReceived(@Nullable String challenge) {
+ if (challenge != null && challenge.length() == 6 && registrationState.state == RegistrationState.State.VERIFYING) {
+ verificationCodeView.clear();
- if (gcmStatus == PlayServicesStatus.SUCCESS) {
- promptForRegistrationStart(self, e164number, true);
- } else if (gcmStatus == PlayServicesStatus.MISSING) {
- promptForNoPlayServices(self, e164number);
- } else if (gcmStatus == PlayServicesStatus.NEEDS_UPDATE) {
- GoogleApiAvailability.getInstance().getErrorDialog(self, ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
- } else {
- Dialogs.showAlertDialog(self, getString(R.string.RegistrationActivity_play_services_error),
- getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
+ try {
+ for (int i=0;i verificationCodeView.append(Integer.parseInt(Character.toString(challenge.charAt(index)))), i * 200);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, e);
+ verificationCodeView.clear();
}
}
+ }
- private void promptForRegistrationStart(final Context context, final String e164number, final boolean gcmSupported) {
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- dialog.setTitle(PhoneNumberFormatter.getInternationalFormatFromE164(e164number));
- dialog.setMessage(R.string.RegistrationActivity_we_will_now_verify_that_the_following_number_is_associated_with_your_device_s);
- dialog.setPositiveButton(getString(R.string.RegistrationActivity_continue),
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- Intent intent = new Intent(context, RegistrationProgressActivity.class);
- intent.putExtra(RegistrationProgressActivity.NUMBER_EXTRA, e164number);
- intent.putExtra(RegistrationProgressActivity.MASTER_SECRET_EXTRA, masterSecret);
- intent.putExtra(RegistrationProgressActivity.GCM_SUPPORTED_EXTRA, gcmSupported);
- startActivity(intent);
- finish();
- }
- });
- dialog.setNegativeButton(getString(R.string.RegistrationActivity_edit), null);
- dialog.show();
- }
+ @SuppressLint("StaticFieldLeak")
+ @Override
+ public void onCodeComplete(@NonNull String code) {
+ this.registrationState = new RegistrationState(RegistrationState.State.CHECKING, this.registrationState);
+ callMeCountDownView.setVisibility(View.INVISIBLE);
+ keyboard.displayProgress();
+
+ new AsyncTask() {
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ int registrationId = KeyHelper.generateRegistrationId(false);
+ TextSecurePreferences.setLocalRegistrationId(RegistrationActivity.this, registrationId);
+ SessionUtil.archiveAllSessions(RegistrationActivity.this);
+
+ String signalingKey = Util.getSecret(52);
- private void promptForNoPlayServices(final Context context, final String e164number) {
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- dialog.setTitle(R.string.RegistrationActivity_missing_google_play_services);
- dialog.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services);
- dialog.setPositiveButton(R.string.RegistrationActivity_i_understand, new DialogInterface.OnClickListener() {
+ accountManager.verifyAccountWithCode(code, signalingKey, registrationId, !registrationState.gcmToken.isPresent());
+
+ IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(RegistrationActivity.this);
+ List records = PreKeyUtil.generatePreKeys(RegistrationActivity.this);
+ SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(RegistrationActivity.this, identityKey, true);
+
+ accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
+ accountManager.setGcmId(registrationState.gcmToken);
+
+ TextSecurePreferences.setGcmRegistrationId(RegistrationActivity.this, registrationState.gcmToken.orNull());
+ TextSecurePreferences.setGcmDisabled(RegistrationActivity.this, !registrationState.gcmToken.isPresent());
+ TextSecurePreferences.setWebsocketRegistered(RegistrationActivity.this, true);
+
+ DatabaseFactory.getIdentityDatabase(RegistrationActivity.this)
+ .saveIdentity(Address.fromSerialized(registrationState.e164number),
+ identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
+ true, System.currentTimeMillis(), true);
+
+ TextSecurePreferences.setVerifying(RegistrationActivity.this, false);
+ TextSecurePreferences.setPushRegistered(RegistrationActivity.this, true);
+ TextSecurePreferences.setLocalNumber(RegistrationActivity.this, registrationState.e164number);
+ TextSecurePreferences.setPushServerPassword(RegistrationActivity.this, registrationState.password);
+ TextSecurePreferences.setSignalingKey(RegistrationActivity.this, signalingKey);
+ TextSecurePreferences.setSignedPreKeyRegistered(RegistrationActivity.this, true);
+ TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
+
+ return true;
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ keyboard.displaySuccess().addListener(new AssertedSuccessListener() {
+ @Override
+ public void onSuccess(Boolean result) {
+ ApplicationContext.getInstance(RegistrationActivity.this).getJobManager().add(new DirectoryRefreshJob(RegistrationActivity.this));
+
+ DirectoryRefreshListener.schedule(RegistrationActivity.this);
+ RotateSignedPreKeyListener.schedule(RegistrationActivity.this);
+
+ Intent nextIntent = getIntent().getParcelableExtra("next_intent");
+
+ if (nextIntent == null) {
+ nextIntent = new Intent(RegistrationActivity.this, ConversationListActivity.class);
+ }
+
+ startActivity(nextIntent);
+ finish();
+ }
+ });
+ } else {
+ keyboard.displayFailure().addListener(new AssertedSuccessListener() {
+ @Override
+ public void onSuccess(Boolean result) {
+ registrationState = new RegistrationState(RegistrationState.State.VERIFYING, registrationState);
+ callMeCountDownView.setVisibility(View.VISIBLE);
+ verificationCodeView.clear();
+ keyboard.displayKeyboard();
+ }
+ });
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private void handlePhoneCallRequest() {
+ if (registrationState.state == RegistrationState.State.VERIFYING) {
+ callMeCountDownView.startCountDown(300);
+
+ new AsyncTask() {
@Override
- public void onClick(DialogInterface dialog, int which) {
- promptForRegistrationStart(context, e164number, false);
+ protected Void doInBackground(Void... voids) {
+ try {
+ accountManager.requestVoiceVerificationCode();
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+
+ return null;
}
- });
- dialog.setNegativeButton(android.R.string.cancel, null);
- dialog.show();
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
+ }
- private PlayServicesStatus checkPlayServices(Context context) {
- int gcmStatus = 0;
+ private void displayInitialView(@NonNull String e164number) {
+ title.animate().translationX(title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ title.setText(R.string.registration_activity__verify_your_number);
+ title.clearAnimation();
+ title.setTranslationX(-1 * title.getWidth());
+ title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
+ }
+ }).start();
- try {
- gcmStatus = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
- } catch (Throwable t) {
- Log.w(TAG, t);
- return PlayServicesStatus.MISSING;
+ subtitle.animate().translationX(subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ subtitle.setText(R.string.registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply);
+ subtitle.clearAnimation();
+ subtitle.setTranslationX(-1 * subtitle.getWidth());
+ subtitle.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
}
+ }).start();
- Log.w(TAG, "Play Services: " + gcmStatus);
+ verificationContainer.animate().translationX(verificationContainer.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ verificationContainer.clearAnimation();
+ verificationContainer.setVisibility(View.INVISIBLE);
+ verificationContainer.setTranslationX(0);
+
+ registrationContainer.setTranslationX(-1 * registrationContainer.getWidth());
+ registrationContainer.setVisibility(View.VISIBLE);
+ createButton.setProgress(0);
+ createButton.setIndeterminateProgressMode(false);
+ registrationContainer.animate().translationX(0).setDuration(SCENE_TRANSITION_DURATION).setListener(null).setInterpolator(new OvershootInterpolator()).start();
+ }
+ }).start();
- switch (gcmStatus) {
- case ConnectionResult.SUCCESS:
- return PlayServicesStatus.SUCCESS;
- case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
- try {
- ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo("com.google.android.gms", 0);
+ fab.animate().rotationBy(360f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ fab.clearAnimation();
+ fab.setImageResource(R.drawable.ic_action_name);
+ fab.animate().rotationBy(375f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
+ }
+ }).start();
+ }
- if (applicationInfo != null && !applicationInfo.enabled) {
- return PlayServicesStatus.MISSING;
- }
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(TAG, e);
+ private void displayVerificationView(@NonNull String e164number, int callCountdown) {
+ ServiceUtil.getInputMethodManager(this)
+ .hideSoftInputFromWindow(countryCode.getWindowToken(), 0);
+
+ ServiceUtil.getInputMethodManager(this)
+ .hideSoftInputFromWindow(number.getWindowToken(), 0);
+
+ title.animate().translationX(-1 * title.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ title.setText(String.format("Verify %s", e164number));
+ title.clearAnimation();
+ title.setTranslationX(title.getWidth());
+ title.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
+ }
+ }).start();
+
+ subtitle.animate().translationX(-1 * subtitle.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ SpannableString subtitleDescription = new SpannableString(String.format("Please enter the verification code sent to %s.", e164number));
+ SpannableString wrongNumber = new SpannableString("Wrong number?");
+
+ ClickableSpan clickableSpan = new ClickableSpan() {
+ @Override
+ public void onClick(View widget) {
+ displayInitialView(e164number);
+ registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
}
- return PlayServicesStatus.NEEDS_UPDATE;
- case ConnectionResult.SERVICE_DISABLED:
- case ConnectionResult.SERVICE_MISSING:
- case ConnectionResult.SERVICE_INVALID:
- case ConnectionResult.API_UNAVAILABLE:
- case ConnectionResult.SERVICE_MISSING_PERMISSION:
- return PlayServicesStatus.MISSING;
- default:
- return PlayServicesStatus.TRANSIENT_ERROR;
+ @Override
+ public void updateDrawState(TextPaint paint) {
+ paint.setColor(Color.WHITE);
+ paint.setUnderlineText(true);
+ }
+ };
+
+ wrongNumber.setSpan(clickableSpan, 0, wrongNumber.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ subtitle.setText(new SpannableStringBuilder(subtitleDescription).append(" ").append(wrongNumber));
+ subtitle.setMovementMethod(LinkMovementMethod.getInstance());
+ subtitle.clearAnimation();
+ subtitle.setTranslationX(subtitle.getWidth());
+ subtitle.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
+ }
+ }).start();
+
+ registrationContainer.animate().translationX(-1 * registrationContainer.getWidth()).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ registrationContainer.clearAnimation();
+ registrationContainer.setVisibility(View.INVISIBLE);
+ registrationContainer.setTranslationX(0);
+
+ verificationContainer.setTranslationX(verificationContainer.getWidth());
+ verificationContainer.setVisibility(View.VISIBLE);
+ verificationContainer.animate().translationX(0).setListener(null).setInterpolator(new OvershootInterpolator()).setDuration(SCENE_TRANSITION_DURATION).start();
+ }
+ }).start();
+
+ fab.animate().rotationBy(-360f).setDuration(SCENE_TRANSITION_DURATION).setListener(new AnimationCompleteListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ fab.clearAnimation();
+ fab.setImageResource(R.drawable.ic_textsms_24dp);
+ fab.animate().rotationBy(-375f).setDuration(SCENE_TRANSITION_DURATION).setListener(null).start();
}
+ }).start();
+
+ this.callMeCountDownView.startCountDown(callCountdown);
+ }
+
+ private void handleCancel() {
+ TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
+ Intent nextIntent = getIntent().getParcelableExtra("next_intent");
+
+ if (nextIntent == null) {
+ nextIntent = new Intent(RegistrationActivity.this, ConversationListActivity.class);
+ }
+
+ startActivity(nextIntent);
+ finish();
+ }
+
+ private void handlePromptForNoPlayServices(@NonNull String e164number) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle(R.string.RegistrationActivity_missing_google_play_services);
+ dialog.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services);
+ dialog.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(e164number, false));
+ dialog.setNegativeButton(android.R.string.cancel, null);
+ dialog.show();
+ }
+
+ private void initializeChallengeListener() {
+ challengeReceiver = new ChallengeReceiver();
+ IntentFilter filter = new IntentFilter(CHALLENGE_EVENT);
+ registerReceiver(challengeReceiver, filter);
+ }
+
+ private void shutdownChallengeListener() {
+ if (challengeReceiver != null) {
+ unregisterReceiver(challengeReceiver);
+ challengeReceiver = null;
+ }
+ }
+
+ private void markAsVerifying(boolean verifying) {
+ TextSecurePreferences.setVerifying(this, verifying);
+
+ if (verifying) {
+ TextSecurePreferences.setPushRegistered(this, false);
+ }
+ }
+
+ private class ChallengeReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.w(TAG, "Got a challenge broadcast...");
+ handleChallengeReceived(intent.getStringExtra(CHALLENGE_EXTRA));
}
}
@@ -358,21 +668,6 @@ public class RegistrationActivity extends BaseActionBarActivity {
}
}
- private class CancelButtonListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- TextSecurePreferences.setPromptedPushRegistration(RegistrationActivity.this, true);
- Intent nextIntent = getIntent().getParcelableExtra("next_intent");
-
- if (nextIntent == null) {
- nextIntent = new Intent(RegistrationActivity.this, ConversationListActivity.class);
- }
-
- startActivity(nextIntent);
- finish();
- }
- }
-
private class InformationToggleListener implements View.OnClickListener {
@Override
public void onClick(View v) {
@@ -385,4 +680,29 @@ public class RegistrationActivity extends BaseActionBarActivity {
}
}
}
+
+ private static class RegistrationState {
+ private enum State {
+ INITIAL, VERIFYING, CHECKING
+ }
+
+ private final State state;
+ private final String e164number;
+ private final String password;
+ private final Optional gcmToken;
+
+ RegistrationState(State state, String e164number, String password, Optional gcmToken) {
+ this.state = state;
+ this.e164number = e164number;
+ this.password = password;
+ this.gcmToken = gcmToken;
+ }
+
+ RegistrationState(State state, RegistrationState previous) {
+ this.state = state;
+ this.e164number = previous.e164number;
+ this.password = previous.password;
+ this.gcmToken = previous.gcmToken;
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java b/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java
deleted file mode 100644
index 7616473a1f..0000000000
--- a/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java
+++ /dev/null
@@ -1,653 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.app.ProgressDialog;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.graphics.Color;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Message;
-import android.support.v7.app.AlertDialog;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.crypto.SessionUtil;
-import org.thoughtcrime.securesms.push.AccountManagerFactory;
-import org.thoughtcrime.securesms.service.RegistrationService;
-import org.thoughtcrime.securesms.util.Dialogs;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
-import org.whispersystems.libsignal.util.KeyHelper;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
-import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
-import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
-import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
-
-import java.io.IOException;
-
-import static org.thoughtcrime.securesms.service.RegistrationService.RegistrationState;
-
-public class RegistrationProgressActivity extends BaseActionBarActivity {
-
- private static final String TAG = RegistrationProgressActivity.class.getSimpleName();
-
- public static final String NUMBER_EXTRA = "e164number";
- public static final String MASTER_SECRET_EXTRA = "master_secret";
- public static final String GCM_SUPPORTED_EXTRA = "gcm_supported";
-
- private static final int FOCUSED_COLOR = Color.parseColor("#ff333333");
- private static final int UNFOCUSED_COLOR = Color.parseColor("#ff808080");
-
- private ServiceConnection serviceConnection = new RegistrationServiceConnection();
- private Handler registrationStateHandler = new RegistrationStateHandler();
- private RegistrationReceiver registrationReceiver = new RegistrationReceiver();
-
- private RegistrationService registrationService;
-
- private LinearLayout registrationLayout;
- private LinearLayout verificationFailureLayout;
- private LinearLayout connectivityFailureLayout;
- private RelativeLayout timeoutProgressLayout;
-
- private ProgressBar registrationProgress;
- private ProgressBar connectingProgress;
- private ProgressBar verificationProgress;
- private ProgressBar generatingKeysProgress;
- private ProgressBar gcmRegistrationProgress;
-
-
- private ImageView connectingCheck;
- private ImageView verificationCheck;
- private ImageView generatingKeysCheck;
- private ImageView gcmRegistrationCheck;
-
- private TextView connectingText;
- private TextView verificationText;
- private TextView registrationTimerText;
- private TextView generatingKeysText;
- private TextView gcmRegistrationText;
-
- private Button verificationFailureButton;
- private Button connectivityFailureButton;
- private Button callButton;
- private Button verifyButton;
-
- private EditText codeEditText;
-
- private MasterSecret masterSecret;
- private boolean gcmSupported;
- private volatile boolean visible;
-
- @Override
- public void onCreate(Bundle bundle) {
- super.onCreate(bundle);
- getSupportActionBar().setTitle(getString(R.string.RegistrationProgressActivity_verifying_number));
- setContentView(R.layout.registration_progress_activity);
-
- initializeResources();
- initializeLinks();
- initializeServiceBinding();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- shutdownServiceBinding();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- handleActivityVisible();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- handleActivityNotVisible();
- }
-
- @Override
- public void onBackPressed() {
-
- }
-
- private void initializeServiceBinding() {
- Intent intent = new Intent(this, RegistrationService.class);
- bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
- }
-
- private void initializeResources() {
- this.masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA);
- this.gcmSupported = getIntent().getBooleanExtra(GCM_SUPPORTED_EXTRA, true);
- this.registrationLayout = (LinearLayout)findViewById(R.id.registering_layout);
- this.verificationFailureLayout = (LinearLayout)findViewById(R.id.verification_failure_layout);
- this.connectivityFailureLayout = (LinearLayout)findViewById(R.id.connectivity_failure_layout);
- this.registrationProgress = (ProgressBar) findViewById(R.id.registration_progress);
- this.connectingProgress = (ProgressBar) findViewById(R.id.connecting_progress);
- this.verificationProgress = (ProgressBar) findViewById(R.id.verification_progress);
- this.generatingKeysProgress = (ProgressBar) findViewById(R.id.generating_keys_progress);
- this.gcmRegistrationProgress = (ProgressBar) findViewById(R.id.gcm_registering_progress);
- this.connectingCheck = (ImageView) findViewById(R.id.connecting_complete);
- this.verificationCheck = (ImageView) findViewById(R.id.verification_complete);
- this.generatingKeysCheck = (ImageView) findViewById(R.id.generating_keys_complete);
- this.gcmRegistrationCheck = (ImageView) findViewById(R.id.gcm_registering_complete);
- this.connectingText = (TextView) findViewById(R.id.connecting_text);
- this.verificationText = (TextView) findViewById(R.id.verification_text);
- this.registrationTimerText = (TextView) findViewById(R.id.registration_timer);
- this.generatingKeysText = (TextView) findViewById(R.id.generating_keys_text);
- this.gcmRegistrationText = (TextView) findViewById(R.id.gcm_registering_text);
- this.verificationFailureButton = (Button) findViewById(R.id.verification_failure_edit_button);
- this.connectivityFailureButton = (Button) findViewById(R.id.connectivity_failure_edit_button);
- this.callButton = (Button) findViewById(R.id.call_button);
- this.verifyButton = (Button) findViewById(R.id.verify_button);
- this.codeEditText = (EditText) findViewById(R.id.telephone_code);
- this.timeoutProgressLayout = (RelativeLayout) findViewById(R.id.timer_progress_layout);
- Button editButton = (Button) findViewById(R.id.edit_button);
-
- editButton.setOnClickListener(new EditButtonListener());
- this.verificationFailureButton.setOnClickListener(new EditButtonListener());
- this.connectivityFailureButton.setOnClickListener(new EditButtonListener());
- }
-
- private void initializeLinks() {
- TextView failureText = (TextView) findViewById(R.id.sms_failed_text);
- String pretext = getString(R.string.registration_progress_activity__signal_timed_out_while_waiting_for_a_verification_sms_message);
- String link = getString(R.string.RegistrationProblemsActivity_possible_problems);
- SpannableString spannableString = new SpannableString(pretext + " " + link);
-
- spannableString.setSpan(new ClickableSpan() {
- @Override
- public void onClick(View widget) {
- new AlertDialog.Builder(RegistrationProgressActivity.this)
- .setTitle(R.string.RegistrationProblemsActivity_possible_problems)
- .setView(R.layout.registration_problems)
- .setNeutralButton(android.R.string.ok, null)
- .show();
- }
- }, pretext.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- failureText.setText(spannableString);
- failureText.setMovementMethod(LinkMovementMethod.getInstance());
- }
-
- private void handleActivityVisible() {
- IntentFilter filter = new IntentFilter(RegistrationService.REGISTRATION_EVENT);
- filter.setPriority(1000);
- registerReceiver(registrationReceiver, filter);
- visible = true;
- }
-
- private void handleActivityNotVisible() {
- unregisterReceiver(registrationReceiver);
- visible = false;
- }
-
- private void handleStateIdle() {
- if (hasNumberDirective()) {
- Intent intent = new Intent(this, RegistrationService.class);
- intent.setAction(RegistrationService.REGISTER_NUMBER_ACTION);
- intent.putExtra(RegistrationService.NUMBER_EXTRA, getNumberDirective());
- intent.putExtra(RegistrationService.MASTER_SECRET_EXTRA, masterSecret);
- intent.putExtra(RegistrationService.GCM_SUPPORTED_EXTRA, gcmSupported);
- startService(intent);
- } else {
- Intent intent = new Intent(this, RegistrationActivity.class);
- intent.putExtra("master_secret", masterSecret);
- startActivity(intent);
- finish();
- }
- }
-
- private void handleStateConnecting() {
- this.registrationLayout.setVisibility(View.VISIBLE);
- this.verificationFailureLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.GONE);
- this.connectingProgress.setVisibility(View.VISIBLE);
- this.connectingCheck.setVisibility(View.INVISIBLE);
- this.verificationProgress.setVisibility(View.INVISIBLE);
- this.verificationCheck.setVisibility(View.INVISIBLE);
- this.generatingKeysProgress.setVisibility(View.INVISIBLE);
- this.generatingKeysCheck.setVisibility(View.INVISIBLE);
- this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
- this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
- this.connectingText.setTextColor(FOCUSED_COLOR);
- this.verificationText.setTextColor(UNFOCUSED_COLOR);
- this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
- this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
- this.timeoutProgressLayout.setVisibility(View.VISIBLE);
- }
-
- private void handleStateVerifying() {
- this.registrationLayout.setVisibility(View.VISIBLE);
- this.verificationFailureLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.GONE);
- this.connectingProgress.setVisibility(View.INVISIBLE);
- this.connectingCheck.setVisibility(View.VISIBLE);
- this.verificationProgress.setVisibility(View.VISIBLE);
- this.verificationCheck.setVisibility(View.INVISIBLE);
- this.generatingKeysProgress.setVisibility(View.INVISIBLE);
- this.generatingKeysCheck.setVisibility(View.INVISIBLE);
- this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
- this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
- this.connectingText.setTextColor(UNFOCUSED_COLOR);
- this.verificationText.setTextColor(FOCUSED_COLOR);
- this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
- this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
- this.registrationProgress.setVisibility(View.VISIBLE);
- this.timeoutProgressLayout.setVisibility(View.VISIBLE);
- }
-
- private void handleStateGeneratingKeys() {
- this.registrationLayout.setVisibility(View.VISIBLE);
- this.verificationFailureLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.GONE);
- this.connectingProgress.setVisibility(View.INVISIBLE);
- this.connectingCheck.setVisibility(View.VISIBLE);
- this.verificationProgress.setVisibility(View.INVISIBLE);
- this.verificationCheck.setVisibility(View.VISIBLE);
- this.generatingKeysProgress.setVisibility(View.VISIBLE);
- this.generatingKeysCheck.setVisibility(View.INVISIBLE);
- this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
- this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
- this.connectingText.setTextColor(UNFOCUSED_COLOR);
- this.verificationText.setTextColor(UNFOCUSED_COLOR);
- this.generatingKeysText.setTextColor(FOCUSED_COLOR);
- this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
- this.registrationProgress.setVisibility(View.INVISIBLE);
- this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
- }
-
- private void handleStateGcmRegistering() {
- this.registrationLayout.setVisibility(View.VISIBLE);
- this.verificationFailureLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.GONE);
- this.connectingProgress.setVisibility(View.INVISIBLE);
- this.connectingCheck.setVisibility(View.VISIBLE);
- this.verificationProgress.setVisibility(View.INVISIBLE);
- this.verificationCheck.setVisibility(View.VISIBLE);
- this.generatingKeysProgress.setVisibility(View.INVISIBLE);
- this.generatingKeysCheck.setVisibility(View.VISIBLE);
- this.gcmRegistrationProgress.setVisibility(View.VISIBLE);
- this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
- this.connectingText.setTextColor(UNFOCUSED_COLOR);
- this.verificationText.setTextColor(UNFOCUSED_COLOR);
- this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
- this.gcmRegistrationText.setTextColor(FOCUSED_COLOR);
- this.registrationProgress.setVisibility(View.INVISIBLE);
- this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
- }
-
- private void handleGcmTimeout(RegistrationState state) {
- handleConnectivityError(state);
- }
-
- private void handleVerificationRequestedVoice(RegistrationState state) {
- handleVerificationTimeout(state);
- verifyButton.setOnClickListener(new VerifyClickListener(state.number, state.password, gcmSupported));
- verifyButton.setEnabled(true);
- codeEditText.setEnabled(true);
- }
-
- private void handleVerificationTimeout(RegistrationState state) {
- this.callButton.setOnClickListener(new CallClickListener(state.number));
- this.verifyButton.setEnabled(false);
- this.codeEditText.setEnabled(false);
- this.registrationLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.GONE);
- this.verificationFailureLayout.setVisibility(View.VISIBLE);
- this.verificationFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
- PhoneNumberFormatter.formatNumberInternational(state.number)));
- }
-
- private void handleConnectivityError(RegistrationState state) {
- this.registrationLayout.setVisibility(View.GONE);
- this.verificationFailureLayout.setVisibility(View.GONE);
- this.connectivityFailureLayout.setVisibility(View.VISIBLE);
- this.connectivityFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
- PhoneNumberFormatter.formatNumberInternational(state.number)));
- }
-
- private void handleMultiRegistrationError(RegistrationState state) {
- handleVerificationTimeout(state);
- Dialogs.showAlertDialog(this, getString(R.string.RegistrationProgressActivity_registration_conflict),
- getString(R.string.RegistrationProgressActivity_this_number_is_already_registered_on_a_different));
- }
-
- private void handleVerificationComplete() {
- if (visible) {
- Toast.makeText(this,
- R.string.RegistrationProgressActivity_registration_complete,
- Toast.LENGTH_LONG).show();
- }
-
- shutdownService();
- Intent intent = new Intent(this, CreateProfileActivity.class);
- intent.putExtra(CreateProfileActivity.NEXT_INTENT, new Intent(this, ConversationListActivity.class));
- startActivity(intent);
- finish();
- }
-
- private void handleTimerUpdate() {
- if (registrationService == null)
- return;
-
- int totalSecondsRemaining = registrationService.getSecondsRemaining();
- int minutesRemaining = totalSecondsRemaining / 60;
- int secondsRemaining = totalSecondsRemaining - (minutesRemaining * 60);
- double percentageComplete = (double)((60 * 2) - totalSecondsRemaining) / (double)(60 * 2);
- int progress = (int)Math.round(((double)registrationProgress.getMax()) * percentageComplete);
-
- this.registrationProgress.setProgress(progress);
- this.registrationTimerText.setText(String.format("%02d:%02d", minutesRemaining, secondsRemaining));
-
- registrationStateHandler.sendEmptyMessageDelayed(RegistrationState.STATE_TIMER, 1000);
- }
-
- private boolean hasNumberDirective() {
- return getIntent().getStringExtra(NUMBER_EXTRA) != null;
- }
-
- private String getNumberDirective() {
- return getIntent().getStringExtra(NUMBER_EXTRA);
- }
-
- private void shutdownServiceBinding() {
- if (serviceConnection != null) {
- unbindService(serviceConnection);
- serviceConnection = null;
- }
- }
-
- private void shutdownService() {
- if (registrationService != null) {
- registrationService.shutdown();
- registrationService = null;
- }
-
- shutdownServiceBinding();
-
- Intent serviceIntent = new Intent(RegistrationProgressActivity.this, RegistrationService.class);
- stopService(serviceIntent);
- }
-
- private class RegistrationServiceConnection implements ServiceConnection {
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
- registrationService = ((RegistrationService.RegistrationServiceBinder)service).getService();
- registrationService.setRegistrationStateHandler(registrationStateHandler);
-
- RegistrationState state = registrationService.getRegistrationState();
- registrationStateHandler.obtainMessage(state.state, state).sendToTarget();
-
- handleTimerUpdate();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- registrationService.setRegistrationStateHandler(null);
- }
- }
-
- private class RegistrationStateHandler extends Handler {
- @Override
- public void handleMessage(Message message) {
- RegistrationState state = (RegistrationState)message.obj;
-
- switch (message.what) {
- case RegistrationState.STATE_IDLE: handleStateIdle(); break;
- case RegistrationState.STATE_CONNECTING: handleStateConnecting(); break;
- case RegistrationState.STATE_VERIFYING: handleStateVerifying(); break;
- case RegistrationState.STATE_TIMER: handleTimerUpdate(); break;
- case RegistrationState.STATE_GENERATING_KEYS: handleStateGeneratingKeys(); break;
- case RegistrationState.STATE_GCM_REGISTERING: handleStateGcmRegistering(); break;
- case RegistrationState.STATE_TIMEOUT: handleVerificationTimeout(state); break;
- case RegistrationState.STATE_COMPLETE: handleVerificationComplete(); break;
- case RegistrationState.STATE_GCM_TIMEOUT: handleGcmTimeout(state); break;
- case RegistrationState.STATE_NETWORK_ERROR: handleConnectivityError(state); break;
- case RegistrationState.STATE_MULTI_REGISTERED: handleMultiRegistrationError(state); break;
- case RegistrationState.STATE_VOICE_REQUESTED: handleVerificationRequestedVoice(state); break;
- }
- }
- }
-
- private class EditButtonListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- shutdownService();
-
- Intent activityIntent = new Intent(RegistrationProgressActivity.this, RegistrationActivity.class);
- activityIntent.putExtra(RegistrationProgressActivity.MASTER_SECRET_EXTRA, masterSecret);
- activityIntent.putExtra(RegistrationProgressActivity.GCM_SUPPORTED_EXTRA, gcmSupported);
- startActivity(activityIntent);
- finish();
- }
- }
-
- private static class RegistrationReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- abortBroadcast();
- }
- }
-
- private class VerifyClickListener implements View.OnClickListener {
-
- private static final int SUCCESS = 0;
- private static final int NETWORK_ERROR = 1;
- private static final int RATE_LIMIT_ERROR = 2;
- private static final int VERIFICATION_ERROR = 3;
- private static final int MULTI_REGISTRATION_ERROR = 4;
-
- private final String e164number;
- private final String password;
- private final String signalingKey;
- private final boolean gcmSupported;
- private final Context context;
-
- private ProgressDialog progressDialog;
-
- public VerifyClickListener(String e164number, String password, boolean gcmSupported) {
- this.e164number = e164number;
- this.password = password;
- this.signalingKey = Util.getSecret(52);
- this.gcmSupported = gcmSupported;
- this.context = RegistrationProgressActivity.this;
- }
-
- @Override
- public void onClick(View v) {
- final String code = codeEditText.getText().toString();
-
- if (TextUtils.isEmpty(code)) {
- Toast.makeText(context,
- getString(R.string.RegistrationProgressActivity_you_must_enter_the_code_you_received_first),
- Toast.LENGTH_LONG).show();
- return;
- }
-
- new AsyncTask() {
-
- @Override
- protected void onPreExecute() {
- progressDialog = ProgressDialog.show(context,
- getString(R.string.RegistrationProgressActivity_connecting),
- getString(R.string.RegistrationProgressActivity_connecting_for_verification),
- true, false);
- }
-
- @Override
- protected void onPostExecute(Integer result) {
- if (progressDialog != null) progressDialog.dismiss();
-
- switch (result) {
- case SUCCESS:
- Intent intent = new Intent(context, RegistrationService.class);
- intent.setAction(RegistrationService.VOICE_REGISTER_ACTION);
- intent.putExtra(RegistrationService.NUMBER_EXTRA, e164number);
- intent.putExtra(RegistrationService.PASSWORD_EXTRA, password);
- intent.putExtra(RegistrationService.SIGNALING_KEY_EXTRA, signalingKey);
- intent.putExtra(RegistrationService.MASTER_SECRET_EXTRA, masterSecret);
- intent.putExtra(RegistrationService.GCM_SUPPORTED_EXTRA, gcmSupported);
- startService(intent);
- break;
- case NETWORK_ERROR:
- Dialogs.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_network_error),
- getString(R.string.RegistrationProgressActivity_unable_to_connect));
- break;
- case VERIFICATION_ERROR:
- Dialogs.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_verification_failed),
- getString(R.string.RegistrationProgressActivity_the_verification_code_you_submitted_is_incorrect));
- break;
- case RATE_LIMIT_ERROR:
- Dialogs.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_too_many_attempts),
- getString(R.string.RegistrationProgressActivity_youve_submitted_an_incorrect_verification_code_too_many_times));
- break;
- case MULTI_REGISTRATION_ERROR:
- Dialogs.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_registration_conflict),
- getString(R.string.RegistrationProgressActivity_this_number_is_already_registered_on_a_different));
- break;
- }
- }
-
- @Override
- protected Integer doInBackground(Void... params) {
- try {
- SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context, e164number, password);
- int registrationId = KeyHelper.generateRegistrationId(false);
-
- TextSecurePreferences.setLocalRegistrationId(context, registrationId);
- SessionUtil.archiveAllSessions(context);
-
- accountManager.verifyAccountWithCode(code, signalingKey, registrationId, !gcmSupported);
-
- return SUCCESS;
- } catch (ExpectationFailedException e) {
- Log.w(TAG, e);
- return MULTI_REGISTRATION_ERROR;
- } catch (RateLimitException e) {
- Log.w(TAG, e);
- return RATE_LIMIT_ERROR;
- } catch (AuthorizationFailedException e) {
- Log.w(TAG, e);
- return VERIFICATION_ERROR;
- } catch (IOException e) {
- Log.w(TAG, e);
- return NETWORK_ERROR;
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- }
-
- private class CallClickListener implements View.OnClickListener {
-
- private static final int SUCCESS = 0;
- private static final int NETWORK_ERROR = 1;
- private static final int RATE_LIMIT_EXCEEDED = 2;
- private static final int CREATE_ERROR = 3;
-
- private final String e164number;
- private final String password;
- private final Context context;
-
- public CallClickListener(String e164number) {
- this.e164number = e164number;
- this.password = Util.getSecret(18);
- this.context = RegistrationProgressActivity.this;
- }
-
- @Override
- public void onClick(View v) {
- new AsyncTask() {
- private ProgressDialog progressDialog;
-
- @Override
- protected void onPreExecute() {
- progressDialog = ProgressDialog.show(context,
- getString(R.string.RegistrationProgressActivity_requesting_call),
- getString(R.string.RegistrationProgressActivity_requesting_incoming_call),
- true, false);
- }
-
- @Override
- protected void onPostExecute(Integer result) {
- if (progressDialog != null) progressDialog.dismiss();
-
- switch (result) {
- case SUCCESS:
- Intent intent = new Intent(context, RegistrationService.class);
- intent.setAction(RegistrationService.VOICE_REQUESTED_ACTION);
- intent.putExtra(RegistrationService.NUMBER_EXTRA, e164number);
- intent.putExtra(RegistrationService.PASSWORD_EXTRA, password);
- intent.putExtra(RegistrationService.MASTER_SECRET_EXTRA, masterSecret);
- intent.putExtra(RegistrationService.GCM_SUPPORTED_EXTRA, gcmSupported);
- startService(intent);
-
- callButton.setEnabled(false);
- new Handler().postDelayed(new Runnable(){
- @Override
- public void run() {
- callButton.setEnabled(true);
- }
- }, 15000);
- break;
- case NETWORK_ERROR:
- Dialogs.showAlertDialog(context,
- getString(R.string.RegistrationProgressActivity_network_error),
- getString(R.string.RegistrationProgressActivity_unable_to_connect));
- break;
- case CREATE_ERROR:
- Dialogs.showAlertDialog(context,
- getString(R.string.RegistrationProgressActivity_server_error),
- getString(R.string.RegistrationProgressActivity_the_server_encountered_an_error));
- break;
- case RATE_LIMIT_EXCEEDED:
- Dialogs.showAlertDialog(context,
- getString(R.string.RegistrationProgressActivity_too_many_requests),
- getString(R.string.RegistrationProgressActivity_youve_already_requested_a_voice_call));
- break;
- }
- }
-
- @Override
- protected Integer doInBackground(Void... params) {
- try {
- SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context, e164number, password);
- accountManager.requestVoiceVerificationCode();
-
- return SUCCESS;
- } catch (RateLimitException e) {
- Log.w(TAG, e);
- return RATE_LIMIT_EXCEEDED;
- } catch (IOException e) {
- Log.w(TAG, e);
- return NETWORK_ERROR;
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- }
-}
diff --git a/src/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java b/src/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java
new file mode 100644
index 0000000000..3063a04c91
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java
@@ -0,0 +1,17 @@
+package org.thoughtcrime.securesms.animation;
+
+
+import android.animation.Animator;
+
+public abstract class AnimationCompleteListener implements Animator.AnimatorListener {
+ @Override
+ public final void onAnimationStart(Animator animation) {}
+
+ @Override
+ public abstract void onAnimationEnd(Animator animation);
+
+ @Override
+ public final void onAnimationCancel(Animator animation) {}
+ @Override
+ public final void onAnimationRepeat(Animator animation) {}
+}
diff --git a/src/org/thoughtcrime/securesms/components/multiwaveview/Tweener.java b/src/org/thoughtcrime/securesms/components/multiwaveview/Tweener.java
index 6b9a39c5df..6015b68b49 100644
--- a/src/org/thoughtcrime/securesms/components/multiwaveview/Tweener.java
+++ b/src/org/thoughtcrime/securesms/components/multiwaveview/Tweener.java
@@ -76,7 +76,7 @@ class Tweener {
interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
} else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
updateListener = (AnimatorUpdateListener) value;
- } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) {
+ } else if ("onCodeComplete".equals(key) || "onCompleteListener".equals(key)) {
listener = (AnimatorListener) value;
} else if ("delay".equals(key)) {
delay = ((Number) value).longValue();
diff --git a/src/org/thoughtcrime/securesms/components/registration/CallMeCountDownView.java b/src/org/thoughtcrime/securesms/components/registration/CallMeCountDownView.java
new file mode 100644
index 0000000000..b6210e112c
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/registration/CallMeCountDownView.java
@@ -0,0 +1,107 @@
+package org.thoughtcrime.securesms.components.registration;
+
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.thoughtcrime.securesms.R;
+
+public class CallMeCountDownView extends RelativeLayout {
+
+ private ImageView phone;
+ private TextView callMeText;
+ private TextView availableInText;
+ private TextView countDownText;
+
+ private int countDown;
+ private OnClickListener listener;
+
+ public CallMeCountDownView(Context context) {
+ super(context);
+ initialize();
+ }
+
+ public CallMeCountDownView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public CallMeCountDownView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public CallMeCountDownView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize();
+ }
+
+ private void initialize() {
+ inflate(getContext(), R.layout.registration_call_me_view, this);
+
+ this.phone = findViewById(R.id.phone_icon);
+ this.callMeText = findViewById(R.id.call_me_text);
+ this.availableInText = findViewById(R.id.available_in_text);
+ this.countDownText = findViewById(R.id.countdown);
+ }
+
+ public void setOnClickListener(@Nullable OnClickListener listener) {
+ this.listener = listener;
+ }
+
+ public void startCountDown(int countDown) {
+ setVisibility(View.VISIBLE);
+ this.phone.setColorFilter(null);
+ this.phone.setOnClickListener(null);
+
+ this.callMeText.setTextColor(getResources().getColor(R.color.grey_700));
+ this.callMeText.setOnClickListener(null);
+
+ this.availableInText.setVisibility(View.VISIBLE);
+ this.countDownText.setVisibility(View.VISIBLE);
+
+ this.countDown = countDown;
+ updateCountDown();
+ }
+
+ public void setCallEnabled() {
+ setVisibility(View.VISIBLE);
+ this.phone.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN));
+ this.callMeText.setTextColor(getResources().getColor(R.color.signal_primary));
+
+ this.availableInText.setVisibility(View.GONE);
+ this.countDownText.setVisibility(View.GONE);
+
+ this.phone.setOnClickListener(v -> handlePhoneCallRequest());
+ this.callMeText.setOnClickListener(v -> handlePhoneCallRequest());
+ }
+
+ private void updateCountDown() {
+ if (countDown > 0) {
+ countDown--;
+
+ int minutesRemaining = countDown / 60;
+ int secondsRemaining = countDown - (minutesRemaining * 60);
+
+ countDownText.setText(String.format("%02d:%02d", minutesRemaining, secondsRemaining));
+ countDownText.postDelayed(this::updateCountDown, 1000);
+ } else if (countDown == 0) {
+ setCallEnabled();
+ }
+ }
+
+ private void handlePhoneCallRequest() {
+ if (listener != null) listener.onClick(this);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/registration/VerificationCodeView.java b/src/org/thoughtcrime/securesms/components/registration/VerificationCodeView.java
new file mode 100644
index 0000000000..4bd363e9ec
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/registration/VerificationCodeView.java
@@ -0,0 +1,162 @@
+package org.thoughtcrime.securesms.components.registration;
+
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.annimon.stream.Collectors;
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VerificationCodeView extends FrameLayout {
+
+ private final List spaces = new ArrayList<>(6);
+ private final List codes = new ArrayList<>(6);
+ private final List containers = new ArrayList<>(7);
+
+ private OnCodeEnteredListener listener;
+ private int index = 0;
+
+ public VerificationCodeView(Context context) {
+ super(context);
+ initialize(context, null);
+ }
+
+ public VerificationCodeView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context, attrs);
+ }
+
+ public VerificationCodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context, attrs);
+ }
+
+ private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
+ inflate(context, R.layout.verification_code_view, this);
+
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerificationCodeView);
+
+ try {
+ TextView separator = findViewById(R.id.separator);
+
+ this.spaces.add(findViewById(R.id.space_zero));
+ this.spaces.add(findViewById(R.id.space_one));
+ this.spaces.add(findViewById(R.id.space_two));
+ this.spaces.add(findViewById(R.id.space_three));
+ this.spaces.add(findViewById(R.id.space_four));
+ this.spaces.add(findViewById(R.id.space_five));
+
+ this.codes.add(findViewById(R.id.code_zero));
+ this.codes.add(findViewById(R.id.code_one));
+ this.codes.add(findViewById(R.id.code_two));
+ this.codes.add(findViewById(R.id.code_three));
+ this.codes.add(findViewById(R.id.code_four));
+ this.codes.add(findViewById(R.id.code_five));
+
+ this.containers.add(findViewById(R.id.container_zero));
+ this.containers.add(findViewById(R.id.container_one));
+ this.containers.add(findViewById(R.id.container_two));
+ this.containers.add(findViewById(R.id.separator_container));
+ this.containers.add(findViewById(R.id.container_three));
+ this.containers.add(findViewById(R.id.container_four));
+ this.containers.add(findViewById(R.id.container_five));
+
+ Stream.of(spaces).forEach(view -> view.setBackgroundColor(typedArray.getColor(R.styleable.VerificationCodeView_vcv_inputColor, Color.BLACK)));
+ Stream.of(spaces).forEach(view -> view.setLayoutParams(new LinearLayout.LayoutParams(typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_inputWidth, ViewUtil.dpToPx(context, 20)),
+ typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_inputHeight, ViewUtil.dpToPx(context, 1)))));
+ Stream.of(codes).forEach(textView -> textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, typedArray.getDimension(R.styleable.VerificationCodeView_vcv_textSize, 30)));
+ Stream.of(codes).forEach(textView -> textView.setTextColor(typedArray.getColor(R.styleable.VerificationCodeView_vcv_textColor, Color.GRAY)));
+
+ Stream.of(containers).forEach(view -> {
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)view.getLayoutParams();
+ params.setMargins(typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_spacing, ViewUtil.dpToPx(context, 5)),
+ params.topMargin, params.rightMargin, params.bottomMargin);
+ view.setLayoutParams(params);
+ });
+
+ separator.setTextSize(TypedValue.COMPLEX_UNIT_SP, typedArray.getDimension(R.styleable.VerificationCodeView_vcv_textSize, 30));
+ } finally {
+ if (typedArray != null) typedArray.recycle();
+ }
+ }
+
+ @MainThread
+ public void setOnCompleteListener(OnCodeEnteredListener listener) {
+ this.listener = listener;
+ }
+
+ @MainThread
+ public void append(int value) {
+ if (index >= codes.size()) return;
+
+ TextView codeView = codes.get(index++);
+
+ Animation translateIn = new TranslateAnimation(0, 0, codeView.getHeight(), 0);
+ translateIn.setInterpolator(new OvershootInterpolator());
+ translateIn.setDuration(500);
+
+ Animation fadeIn = new AlphaAnimation(0, 1);
+ fadeIn.setDuration(200);
+
+ AnimationSet animationSet = new AnimationSet(false);
+ animationSet.addAnimation(fadeIn);
+ animationSet.addAnimation(translateIn);
+ animationSet.reset();
+ animationSet.setStartTime(0);
+
+ codeView.setText(String.valueOf(value));
+ codeView.clearAnimation();
+ codeView.startAnimation(animationSet);
+
+ if (index == codes.size() && listener != null) {
+ listener.onCodeComplete(Stream.of(codes).map(TextView::getText).collect(Collectors.joining()));
+ }
+ }
+
+ @MainThread
+ public void delete() {
+ if (index <= 0) return;
+ codes.get(--index).setText("");
+ }
+
+ @MainThread
+ public void clear() {
+ if (index != 0) {
+ Stream.of(codes).forEach(code -> code.setText(""));
+ index = 0;
+ }
+ }
+
+ public interface OnCodeEnteredListener {
+ void onCodeComplete(@NonNull String code);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/registration/VerificationPinKeyboard.java b/src/org/thoughtcrime/securesms/components/registration/VerificationPinKeyboard.java
new file mode 100644
index 0000000000..b22296e6fe
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/registration/VerificationPinKeyboard.java
@@ -0,0 +1,174 @@
+package org.thoughtcrime.securesms.components.registration;
+
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.KeyboardView;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
+import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
+
+public class VerificationPinKeyboard extends FrameLayout {
+
+ private KeyboardView keyboardView;
+ private ProgressBar progressBar;
+ private ImageView successView;
+ private ImageView failureView;
+
+ private OnKeyPressListener listener;
+
+ public VerificationPinKeyboard(@NonNull Context context) {
+ super(context);
+ initialize();
+ }
+
+ public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initialize();
+ }
+
+ public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize();
+ }
+
+ private void initialize() {
+ inflate(getContext(), R.layout.verification_pin_keyboard_view, this);
+
+ this.keyboardView = findViewById(R.id.keyboard_view);
+ this.progressBar = findViewById(R.id.progress);
+ this.successView = findViewById(R.id.success);
+ this.failureView = findViewById(R.id.failure); ;
+
+ keyboardView.setPreviewEnabled(false);
+ keyboardView.setKeyboard(new Keyboard(getContext(), R.xml.pin_keyboard));
+ keyboardView.setOnKeyboardActionListener(new KeyboardView.OnKeyboardActionListener() {
+ @Override
+ public void onPress(int primaryCode) {
+ if (listener != null) listener.onKeyPress(primaryCode);
+ }
+ @Override
+ public void onRelease(int primaryCode) {}
+ @Override
+ public void onKey(int primaryCode, int[] keyCodes) {}
+ @Override
+ public void onText(CharSequence text) {}
+ @Override
+ public void swipeLeft() {}
+ @Override
+ public void swipeRight() {}
+ @Override
+ public void swipeDown() {}
+ @Override
+ public void swipeUp() {}
+ });
+
+ displayKeyboard();
+ }
+
+ public void setOnKeyPressListener(@Nullable OnKeyPressListener listener) {
+ this.listener = listener;
+ }
+
+ public void displayKeyboard() {
+ this.keyboardView.setVisibility(View.VISIBLE);
+ this.progressBar.setVisibility(View.GONE);
+ this.successView.setVisibility(View.GONE);
+ this.failureView.setVisibility(View.GONE);
+ }
+
+ public void displayProgress() {
+ this.keyboardView.setVisibility(View.INVISIBLE);
+ this.progressBar.setVisibility(View.VISIBLE);
+ this.successView.setVisibility(View.GONE);
+ this.failureView.setVisibility(View.GONE);
+ }
+
+ public ListenableFuture displaySuccess() {
+ SettableFuture result = new SettableFuture<>();
+
+ this.keyboardView.setVisibility(View.INVISIBLE);
+ this.progressBar.setVisibility(View.GONE);
+ this.failureView.setVisibility(View.GONE);
+
+ this.successView.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
+
+ ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
+ ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
+ ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
+ scaleAnimation.setInterpolator(new OvershootInterpolator());
+ scaleAnimation.setDuration(800);
+ scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ result.set(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ ViewUtil.animateIn(this.successView, scaleAnimation);
+ return result;
+ }
+
+ public ListenableFuture displayFailure() {
+ SettableFuture result = new SettableFuture<>();
+
+ this.keyboardView.setVisibility(View.INVISIBLE);
+ this.progressBar.setVisibility(View.GONE);
+ this.failureView.setVisibility(View.GONE);
+
+ this.failureView.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
+ this.failureView.setVisibility(View.VISIBLE);
+
+ TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
+ shake.setDuration(50);
+ shake.setRepeatCount(7);
+ shake.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ result.set(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ this.failureView.startAnimation(shake);
+
+ return result;
+ }
+
+ public interface OnKeyPressListener {
+ void onKeyPress(int keyCode);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/util/PlayServicesUtil.java b/src/org/thoughtcrime/securesms/util/PlayServicesUtil.java
new file mode 100644
index 0000000000..10e4d6d95d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/PlayServicesUtil.java
@@ -0,0 +1,61 @@
+package org.thoughtcrime.securesms.util;
+
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+public class PlayServicesUtil {
+
+ private static final String TAG = PlayServicesUtil.class.getSimpleName();
+
+ public enum PlayServicesStatus {
+ SUCCESS,
+ MISSING,
+ NEEDS_UPDATE,
+ TRANSIENT_ERROR
+ }
+
+ public static PlayServicesStatus getPlayServicesStatus(Context context) {
+ int gcmStatus = 0;
+
+ try {
+ gcmStatus = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return PlayServicesStatus.MISSING;
+ }
+
+ Log.w(TAG, "Play Services: " + gcmStatus);
+
+ switch (gcmStatus) {
+ case ConnectionResult.SUCCESS:
+ return PlayServicesStatus.SUCCESS;
+ case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
+ try {
+ ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo("com.google.android.gms", 0);
+
+ if (applicationInfo != null && !applicationInfo.enabled) {
+ return PlayServicesStatus.MISSING;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, e);
+ }
+
+ return PlayServicesStatus.NEEDS_UPDATE;
+ case ConnectionResult.SERVICE_DISABLED:
+ case ConnectionResult.SERVICE_MISSING:
+ case ConnectionResult.SERVICE_INVALID:
+ case ConnectionResult.API_UNAVAILABLE:
+ case ConnectionResult.SERVICE_MISSING_PERMISSION:
+ return PlayServicesStatus.MISSING;
+ default:
+ return PlayServicesStatus.TRANSIENT_ERROR;
+ }
+ }
+
+}