From 90ff0e58b0206ac2b047c3db70980f29a533b9d9 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 8 Nov 2017 12:20:11 -0800 Subject: [PATCH] Update registration flow --- AndroidManifest.xml | 7 +- res/anim/slide_to_left.xml | 9 + res/drawable-hdpi/ic_action_name.png | Bin 0 -> 297 bytes res/drawable-hdpi/ic_text_sms.png | Bin 0 -> 311 bytes res/drawable-mdpi/ic_action_name.png | Bin 0 -> 207 bytes res/drawable-mdpi/ic_text_sms.png | Bin 0 -> 224 bytes res/drawable-xhdpi/ic_action_name.png | Bin 0 -> 335 bytes res/drawable-xhdpi/ic_text_sms.png | Bin 0 -> 342 bytes res/drawable-xxhdpi/ic_action_name.png | Bin 0 -> 563 bytes res/drawable-xxhdpi/ic_text_sms.png | Bin 0 -> 545 bytes res/layout/registration_activity.xml | 294 +++++--- res/layout/registration_call_me_view.xml | 45 ++ res/layout/verification_code_view.xml | 154 ++++ res/layout/verification_pin_keyboard_view.xml | 43 ++ res/values/attrs.xml | 10 + res/values/strings.xml | 2 + res/xml/pin_keyboard.xml | 27 + .../securesms/RegistrationActivity.java | 662 +++++++++++++----- .../RegistrationProgressActivity.java | 653 ----------------- .../animation/AnimationCompleteListener.java | 17 + .../components/multiwaveview/Tweener.java | 2 +- .../registration/CallMeCountDownView.java | 107 +++ .../registration/VerificationCodeView.java | 162 +++++ .../registration/VerificationPinKeyboard.java | 174 +++++ .../securesms/util/PlayServicesUtil.java | 61 ++ 25 files changed, 1485 insertions(+), 944 deletions(-) create mode 100644 res/anim/slide_to_left.xml create mode 100644 res/drawable-hdpi/ic_action_name.png create mode 100644 res/drawable-hdpi/ic_text_sms.png create mode 100644 res/drawable-mdpi/ic_action_name.png create mode 100644 res/drawable-mdpi/ic_text_sms.png create mode 100644 res/drawable-xhdpi/ic_action_name.png create mode 100644 res/drawable-xhdpi/ic_text_sms.png create mode 100644 res/drawable-xxhdpi/ic_action_name.png create mode 100644 res/drawable-xxhdpi/ic_text_sms.png create mode 100644 res/layout/registration_call_me_view.xml create mode 100644 res/layout/verification_code_view.xml create mode 100644 res/layout/verification_pin_keyboard_view.xml create mode 100644 res/xml/pin_keyboard.xml delete mode 100644 src/org/thoughtcrime/securesms/RegistrationProgressActivity.java create mode 100644 src/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java create mode 100644 src/org/thoughtcrime/securesms/components/registration/CallMeCountDownView.java create mode 100644 src/org/thoughtcrime/securesms/components/registration/VerificationCodeView.java create mode 100644 src/org/thoughtcrime/securesms/components/registration/VerificationPinKeyboard.java create mode 100644 src/org/thoughtcrime/securesms/util/PlayServicesUtil.java 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 0000000000000000000000000000000000000000..798948971c59c01531051add789bdae8899e8421 GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt51uZLAr-gY&hq6tWFX+?ywHBf z>s9Wjd3U{7xk(^jkyUipN54WQVU8r-Nj@guxcB&K8h@U3;jH-5Z+q5k6)o&oa>};E zIpdIkabb7Lv#51x%C>(T=5*{9SoclAw8mS8wf*}JxwS0~=M46^dvna_VN(10!L*Q1 zO6Q2Gp?ARjud9N&O70vnpYbEoYuSQ*udjM?^)R`Yv@ozJFfejBFfa);pfW$aXS(E& zaGfc7;k|H{dCYyL>sGv73Wjq&fA=bCsuFb}Ue5nguYl+aVQyNxb8 k!+GOQOew$l`1&>GsQDj~Mb=lH1^S)A)78&qol`;+0FLZ*I{*Lx literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_text_sms.png b/res/drawable-hdpi/ic_text_sms.png new file mode 100644 index 0000000000000000000000000000000000000000..6a818fab6f1911eb71d7bd981c825b1891901bb0 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtf1WOmAr-gY&UEB!G7xbwWK+%I z5Wg^c=QB06J11_mc3JNc=412yyj7(4)cSKOn;+IE#5r^72D${!558<-IH%{$kEsv1 zceL=n}KqBf2&x8ASLb*1`@7eo~F=M|N_uKlE7>4N|KB$B+ z{Cz0P@JoKST!Yn&iRBC}qHzo&fgMgCEcbBbJ<)vCU&^K~rhJ#bw_pb_Bp5th{an^L HB{Ts5N~nC+ literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_action_name.png b/res/drawable-mdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..eeefcc09d3dfbe15192c9c84e2eab3ad38cd2d9f GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJX`U{QAr-fh6C~a|`2YVulfxW_ zBf1WEJXP`a z^CUyMk)h9iVN1RSX$MECrs{+%jEngsN)N1Hc9S)D)ey+uW0jC$-N03mVReABzwz_N z61QiM1O3AmhfEid7vwP9H0vqzZ9~Zq>uxT1G3OF91H;81n~Sey&U65}kipZ{&t;uc GLK6V~Q%}4A literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_text_sms.png b/res/drawable-mdpi/ic_text_sms.png new file mode 100644 index 0000000000000000000000000000000000000000..5c5a0f3cea65324b99ca99d58f22a0006b4b68ce GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ<(@8%Ar-fh6C_wO9{m6R|CGZU zmLumvjwu*V<&2!iek47k=W}7?I*udDUp{Csj&Nqu$naM`?RbN8HuId9FBqN&viDde zoUl9~=_%{8M%Z(Wu$tehO6OyYtRAUWJ~?)iZq%e4UbrK{M6k~);R<8(O~x|jmkrJi zItBuK45r`1m^W&!GgCBUUvhb~gTe~DWM4ft8!Aa literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_action_name.png b/res/drawable-xhdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..28cd18b55b5f7d99c06a6a3224b94444fa13bfb8 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU=;IoaSW-r_4c+QSF?jaYvR_+ zw`M0C_{qw`+2Zv4naAb!O}Qh|7T0aRyT`Ofai_aXUDx}C9gG(w zBd zhAlw>tU8O~ZX4Tg`D@9)c+ZFL)w(VBv(2n7an4e`K8Rj3}1v1Oi)z4*}Q$iB}u+Db6 literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_text_sms.png b/res/drawable-xhdpi/ic_text_sms.png new file mode 100644 index 0000000000000000000000000000000000000000..d95eb93192b83db520e46d9d22248aeb877dd313 GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV3hTAaSW-r_4bw_*C7XymWK`IhNhGI;{b0rNA6*Il(*(|)>!x-0HihNd9>k(QCgq z7jJ$X&GbU{K9j@uANmZxj@L76Fucc{kiLicz_lN24SPT4GngH}&ak>ez^UV(4P%zW Vjn3P%&ai;Y_jL7hS?83{1OV|NgL?n~ literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_action_name.png b/res/drawable-xxhdpi/ic_action_name.png new file mode 100644 index 0000000000000000000000000000000000000000..c60efa5acb06749f5609b8754b513fe241358730 GIT binary patch literal 563 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VEpCj;uunK>+N0ttV00;$3MO} zBgQ;fg+~+vSy_zd8(E(tA8<&Jbj?}q+5_z-z2Y3HU(O5mtMK|>4n1+hnXqrD^=eg z3(j62sdG5qJ5HA8U$)cv?DNg@y|zu-eE8vZ%h!_2#mVe#<{p!aDH{_!g`A=zTU)nI;d<>6U} zCjUE@!H}m5bo)G&(|p}l(|<|Veod}xbiS|cV-;Ii|DoSx|Dh%CRqd`ZrVx>t_vB=T`bt@zeb?ePt1P=TeM>>{ zJrA#wfkCA|&Rq@%2Y%6K{b<{!C9?ZIf0sOq7QsBRtGmbsc2RYKy)z4*} HQ$iB}XJ^j% literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_text_sms.png b/res/drawable-xxhdpi/ic_text_sms.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce74424648a9fc2f7dcdbb884fabb6f89147008 GIT binary patch literal 545 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V0_`};uunK>+Ri*UNV6aZ4Y~! zTogAYa-IMmwdg~9@ z58U-Bfv#`9=+#^Qkopty$;-=qd93P@gMCx}Ul)3|_tbrUxv5rJGH0teEJB`TJX`xT zpDn#h=4|C<-K@QzcLqGkIHmh4ue-tjAirzhG8=WDXqG9<_h`F5AbT!G9E|2o|ek8rSQ(KOnJt~n$flAI8HqF?zeU3lsNABVPeDA&l}$CtTboT ze`NO~uKWL;>g!*`7@wL?YZvwu{18&NVdYU7##8=632#)m49X_4YS?==Y@Dod;D)Ez zf - - - + 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; + } + } + +}