Improved registration flow.

master
Alan Evans 2019-10-16 18:37:08 -04:00 committed by Greyson Parrelli
parent a135e7efa2
commit 86d088bce2
59 changed files with 3582 additions and 1971 deletions

View File

@ -119,9 +119,6 @@
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
android:launchMode="singleTask"/>
<activity android:name=".CountrySelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity"
android:theme="@style/TextSecure.HighlightTheme"
android:windowSoftInputMode="stateHidden"
@ -319,24 +316,12 @@
</intent-filter>
</activity>
<activity android:name=".registration.WelcomeActivity"
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".registration.CaptchaActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"

View File

@ -11,10 +11,12 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
}
}
apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness'
apply from: 'witness-verifications.gradle'
@ -66,8 +68,11 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.0.0'
implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui:2.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-alpha04"
implementation "androidx.camera:camera-camera2:1.0.0-alpha04"

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".registration.RegistrationNavigationActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/registration" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/registration_captcha_title"
style="@style/Signal.Text.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/RegistrationActivity_we_need_to_verify_that_youre_human"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<WebView
android:id="@+id/registration_captcha_web_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registration_captcha_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,9 @@
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:src="@drawable/ic_add_white_original_24dp"
android:tint="@color/core_grey_60"/>
android:tint="@color/core_grey_60"
android:contentDescription="+"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/input"
@ -28,6 +30,8 @@
android:inputType="number"
android:maxLength="3"
android:digits="1234567890"
android:saveEnabled="false"
android:hint="@string/RegistrationActivity_country_code_description"
tools:text="123" />
</LinearLayout>

View File

@ -1,28 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dip"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/country_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:textAlignment="viewStart"
android:textColor="@color/core_black"
android:textSize="18sp"
tools:text="Canada" />
<TextView android:id="@+id/country_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:textSize="18sp"
android:textStyle="bold" />
<TextView android:id="@+id/country_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18.0sp"
android:textStyle="bold"
android:textColor="#7b7b7b"
android:gravity="right|center"
android:layout_marginStart="4dp"
android:layout_marginEnd="5dp"
android:layout_gravity="right|center"/>
<TextView
android:id="@+id/country_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="5dp"
android:textColor="@color/core_black"
android:textSize="18sp"
tools:text="+1" />
</LinearLayout>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<fragment android:id="@+id/fragment_content"
android:name="org.thoughtcrime.securesms.CountrySelectionFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

View File

@ -1,20 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/restore_passphrase_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_backup_passphrase_dialog__backup_passphrase"
android:imeOptions="actionDone"
android:inputType="number" />
android:id="@+id/restore_passphrase_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:hint="@string/enter_backup_passphrase_dialog__backup_passphrase"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLength="30"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/signal_primary">
<ImageView
android:id="@+id/watermark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:importantForAccessibility="no"
android:src="@drawable/icon_transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<TextView
android:id="@+id/registration_captcha_title"
style="@style/Signal.Text.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:gravity="center"
android:text="@string/RegistrationActivity_we_need_to_verify_that_youre_human"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<WebView
android:id="@+id/registration_captcha_web_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registration_captcha_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#ffffff">
<EditText android:id="@+id/country_search"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_menu_search_holo_light"
android:hint="@string/country_selection_fragment__search" />
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:divider="#ffdddddd"
android:dividerHeight="1.0px"
android:choiceMode="singleChoice" />
<TextView android:id="@android:id/empty"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/country_selection_fragment__loading_countries" />
</LinearLayout>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".registration.fragments.EnterCodeFragment">
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".registration.fragments.EnterCodeFragment">
<Button
android:id="@+id/wrong_number"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_toEndOf="@id/call_me_count_down"
android:text="@string/RegistrationActivity_wrong_number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/call_me_count_down"
app:layout_constraintTop_toBottomOf="@+id/code" />
<Button
android:id="@+id/no_code"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_toEndOf="@id/call_me_count_down"
android:text="@string/RegistrationActivity_contact_signal_support"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/call_me_count_down"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wrong_number"
tools:visibility="visible" />
<TextView
android:id="@+id/verify_header"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="32dp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/RegistrationActivity_enter_the_code_we_sent_to_s" />
<org.thoughtcrime.securesms.components.registration.CallMeCountDownView
android:id="@+id/call_me_count_down"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/code"
app:layout_constraintEnd_toStartOf="@+id/wrong_number"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/wrong_number"
tools:text="@string/RegistrationActivity_call" />
<org.thoughtcrime.securesms.components.registration.VerificationCodeView
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/verify_header" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard
android:id="@+id/keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".registration.fragments.EnterPhoneNumberFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<FrameLayout
android:id="@+id/country_spinner_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/verify_subheader">
<Spinner
android:id="@+id/country_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAlignment="viewStart" />
</FrameLayout>
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layoutDirection="ltr"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/country_spinner_frame">
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/country_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_weight="1"
app:labeledEditText_background="@color/white"
app:labeledEditText_textLayout="@layout/country_code_text" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
app:labeledEditText_background="@color/white"
app:labeledEditText_label="Phone Number"
app:labeledEditText_textLayout="@layout/phone_text" />
</LinearLayout>
<TextView
android:id="@+id/verify_header"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_enter_your_phone_number_to_get_started"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.Body.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_you_will_receive_a_verification_code"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/cancel_button"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@android:string/cancel"
android:textColor="@color/core_grey_60"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registerButton"
app:layout_constraintVertical_bias="0" />
<com.dd.CircularProgressButton
android:id="@+id/registerButton"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
app:cpb_textIdle="@string/RegistrationActivity_next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".registration.fragments.RegistrationLockFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<TextView
android:id="@+id/clarification_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/registration_activity__the_registration_lock_pin_is_not_the_same_as_the_sms_verification_code_you_just_received_please_enter_the_pin_you_previously_configured_in_the_application"
android:textColor="#73B7F0"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:visibility="visible" />
<TextView
android:id="@+id/verify_header"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_enter_your_phone_number_to_get_started"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.Body.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_you_will_receive_a_verification_code"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="@+id/forgot_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:padding="16dp"
android:text="@string/registration_activity__forgot_pin"
android:textColor="@color/blue_400"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pinButton" />
<com.dd.CircularProgressButton
android:id="@+id/pinButton"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:cpb_textIdle="@string/RegistrationActivity_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/clarification_label">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="@string/registration_activity__registration_lock_pin"
android:imeOptions="actionDone"
android:inputType="numberPassword" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".registration.fragments.RestoreBackupFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<Button
android:id="@+id/skip_restore_button"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:paddingStart="30dp"
android:paddingTop="10dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
android:text="@string/registration_activity__skip"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restore_button" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.Body.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_restore_your_messages_and_media_from_a_local_backup"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="@+id/backup_created_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="@+id/verify_subheader"
app:layout_constraintTop_toBottomOf="@+id/verify_subheader"
tools:text="Backup created: 1 min ago" />
<TextView
android:id="@+id/backup_size_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@+id/backup_created_text"
app:layout_constraintTop_toBottomOf="@+id/backup_created_text"
tools:text="Backup size: 899 KB" />
<TextView
android:id="@+id/backup_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backup_size_text"
tools:text="100 messages so far..." />
<TextView
android:id="@+id/verify_header"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_restore_from_backup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.dd.CircularProgressButton
android:id="@+id/restore_button"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:cpb_textIdle="@string/registration_activity__restore_backup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backup_progress_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1,29 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="32dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:layout_margin="16dp"
android:importantForAccessibility="no"
android:src="@drawable/welcome"
app:layout_constraintBottom_toTopOf="@+id/textView"
app:layout_constraintBottom_toTopOf="@+id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:id="@+id/title"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:layout_marginBottom="40dp"
android:gravity="center"
android:text="@string/RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message"
app:layout_constraintBottom_toTopOf="@+id/welcome_terms_button"
@ -32,10 +30,10 @@
<TextView
android:id="@+id/welcome_terms_button"
style="@style/Signal.Text.Body.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Signal.Text.Body"
android:text="@string/RegistrationActivity_terms_and_privacy"
android:textColor="@color/signal_primary"
app:layout_constraintBottom_toTopOf="@+id/welcome_continue_button"
@ -44,19 +42,12 @@
<com.dd.CircularProgressButton
android:id="@+id/welcome_continue_button"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="54dp"
android:background="@color/signal_primary"
android:textColor="@color/white"
android:elevation="4dp"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
android:layout_marginBottom="@dimen/registration_button_bottom_margin"
app:cpb_textIdle="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/signal_primary">
<ImageView
android:id="@+id/watermark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:importantForAccessibility="no"
android:src="@drawable/icon_transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintVertical_bias="0.4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -12,4 +12,6 @@
android:background="@color/transparent"
android:singleLine="true"
android:inputType="phone"
android:saveEnabled="false"
android:hint="@string/RegistrationActivity_phone_number_description"
tools:text="867-5309" />

View File

@ -1,333 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/white"
android:fillViewport="true"
tools:context=".RegistrationActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_alignParentTop="true"
android:orientation="vertical">
<TextView
android:id="@+id/verify_header"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingBottom="16dp"
android:text="@string/RegistrationActivity_enter_your_phone_number_to_get_started" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="25dp"
android:text="@string/RegistrationActivity_you_will_receive_a_verification_code" />
</LinearLayout>
<LinearLayout
android:id="@+id/restore_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header"
android:layout_marginTop="30dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:orientation="vertical"
android:paddingBottom="0dp"
android:visibility="invisible"
tools:visibility="gone">
<TextView
android:id="@+id/backup_created_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Backup created: 1 min ago" />
<TextView
android:id="@+id/backup_size_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
tools:text="Backup size: 899 KB" />
<TextView
android:id="@+id/backup_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
tools:text="100 messages so far..." />
<com.dd.CircularProgressButton
android:id="@+id/restore_button"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/registration_activity__restore_backup" />
<Button
android:id="@+id/skip_restore_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="13dp"
android:paddingStart="30dp"
android:paddingTop="10dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
style="@style/Button.Borderless.Registration"
android:text="@string/registration_activity__skip" />
</LinearLayout>
<LinearLayout
android:id="@+id/registration_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header"
android:layout_marginStart="32dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="32dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:paddingBottom="0dp"
android:clipToPadding="false"
android:clipChildren="false"
tools:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/labeled_edit_text_background_inactive">
<Spinner
android:id="@+id/country_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="ltr"
android:orientation="horizontal">
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/country_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="12dp"
app:labeledEditText_background="@color/white"
app:labeledEditText_textLayout="@layout/country_code_text" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
app:labeledEditText_background="@color/white"
app:labeledEditText_label="Phone Number"
app:labeledEditText_textLayout="@layout/phone_text"/>
</LinearLayout>
<com.dd.CircularProgressButton
android:id="@+id/registerButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/RegistrationActivity_next" />
<TextView
android:id="@+id/skip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="@android:string/cancel"
android:textColor="@color/core_grey_60" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/verification_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/header"
android:visibility="invisible"
tools:visibility="visible">
<org.thoughtcrime.securesms.components.registration.VerificationCodeView
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:vcv_inputColor="@color/core_black"
app:vcv_inputWidth="30dp"
app:vcv_spacing="10dp"
app:vcv_textColor="@color/core_black" />
<org.thoughtcrime.securesms.components.registration.CallMeCountDownView
android:id="@+id/call_me_count_down"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/code"
app:layout_constraintTop_toTopOf="@+id/wrong_number"
app:layout_constraintStart_toStartOf="@+id/code" />
<Button
android:id="@+id/wrong_number"
style="@style/Button.Borderless.Registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_toEndOf="@id/call_me_count_down"
android:text="@string/RegistrationActivity_wrong_number"
app:layout_constraintEnd_toEndOf="@+id/code"
app:layout_constraintTop_toBottomOf="@+id/code" />
<org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard
android:id="@+id/keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/pin_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/header"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="30dp"
android:paddingEnd="16dp"
android:paddingBottom="0dp"
android:visibility="invisible"
tools:visibility="gone">
<LinearLayout
android:id="@+id/pin_clarification_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/clarification_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="5dp"
android:layout_marginTop="-2dp"
android:layout_marginEnd="5dp"
android:text="@string/registration_activity__the_registration_lock_pin_is_not_the_same_as_the_sms_verification_code_you_just_received_please_enter_the_pin_you_previously_configured_in_the_application"
android:textColor="#73B7F0" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:paddingStart="100dp"
android:paddingEnd="100dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/registration_activity__registration_lock_pin"
android:imeOptions="actionDone"
android:inputType="numberPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.dd.CircularProgressButton
android:id="@+id/pinButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="16dp"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/RegistrationActivity_continue" />
<TextView
android:id="@+id/forgot_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:padding="15dp"
android:text="@string/registration_activity__forgot_pin"
android:textColor="@color/blue_400" />
</LinearLayout>
</RelativeLayout>
</ScrollView>

View File

@ -1,135 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
android:layout_gravity="center"
android:layoutDirection="ltr">
<LinearLayout
<FrameLayout
android:id="@+id/container_zero"
android:layout_width="48dp"
android:layout_height="54dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_zero"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
tools:text="1" />
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="0" />
</LinearLayout>
</FrameLayout>
<LinearLayout
<FrameLayout
android:id="@+id/container_one"
android:layout_width="48dp"
android:layout_height="54dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/container_zero"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
tools:text="2" />
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="1" />
</LinearLayout>
</FrameLayout>
<LinearLayout
<FrameLayout
android:id="@+id/container_two"
android:layout_width="48dp"
android:layout_height="54dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/container_one"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="2" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/separator_container"
<TextView
android:id="@+id/separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center_horizontal"
android:layout_gravity="center"
android:orientation="vertical">
android:layout_marginStart="4dp"
android:text="-"
android:textColor="@color/core_black"
android:textSize="28sp"
app:layout_constraintStart_toEndOf="@+id/container_two"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-"
android:textSize="28dp" />
</LinearLayout>
<LinearLayout
<FrameLayout
android:id="@+id/container_three"
android:layout_width="48dp"
android:layout_height="54dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/separator"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
tools:text="2" />
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="3" />
</LinearLayout>
</FrameLayout>
<LinearLayout
<FrameLayout
android:id="@+id/container_four"
android:layout_width="48dp"
android:layout_height="54dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/container_three"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_four"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
tools:text="2" />
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="4" />
</LinearLayout>
</FrameLayout>
<LinearLayout
<FrameLayout
android:id="@+id/container_five"
android:layout_width="48dp"
android:layout_height="54dp"
android:layout_marginStart="5dp"
android:gravity="center"
android:orientation="vertical"
android:background="@drawable/labeled_edit_text_background_inactive">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:background="@drawable/labeled_edit_text_background_inactive"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/container_four"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/code_five"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28dp"
android:gravity="center"
tools:text="2" />
android:layout_gravity="center"
android:textColor="@color/core_black"
android:textSize="28sp"
tools:text="5" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signup"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.WelcomeFragment"
android:label="fragment_welcome"
tools:layout="@layout/fragment_registration_welcome">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/enterPhoneNumberFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.EnterPhoneNumberFragment"
android:label="fragment_enter_phone_number"
tools:layout="@layout/fragment_registration_enter_phone_number">
<action
android:id="@+id/action_pickCountry"
app:destination="@id/countryPickerFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_enterVerificationCode"
app:destination="@id/enterCodeFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
android:label="fragment_country_picker"
tools:layout="@layout/fragment_registration_country_picker">
<action
android:id="@+id/action_countrySelected"
app:popUpTo="@id/countryPickerFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/enterCodeFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.EnterCodeFragment"
android:label="fragment_enter_code"
tools:layout="@layout/fragment_registration_enter_code">
<action
android:id="@+id/action_requireRegistrationLockPin"
app:destination="@id/registrationLockFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_wrongNumber"
app:popUpTo="@id/enterCodeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/registrationLockFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationLockFragment"
android:label="fragment_registration_lock"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<argument
android:name="timeRemaining"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/captchaFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
android:label="fragment_captcha"
tools:layout="@layout/fragment_registration_captcha">
<action
android:id="@+id/action_captchaComplete"
app:popUpTo="@id/captchaFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/restoreBackupFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment"
android:label="fragment_restore_backup"
tools:layout="@layout/fragment_registration_restore_backup">
<action
android:id="@+id/action_backupRestored"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
<action
android:id="@+id/action_noBackupFound"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip_no_return"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/restoreBackupFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/registrationCompletePlaceHolderFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationCompleteFragment"
android:label="fragment_registration_complete_place_holder"
tools:layout="@layout/fragment_registration_blank" />
</navigation>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="registration_headline_text">28sp</dimen>
<dimen name="registration_body_text">16sp</dimen>
<dimen name="registration_button_bottom_margin">54dp</dimen>
</resources>

View File

@ -315,15 +315,6 @@
</attr>
</declare-styleable>
<declare-styleable name="VerificationCodeView">
<attr name="vcv_spacing" format="dimension"/>
<attr name="vcv_inputWidth" format="dimension"/>
<attr name="vcv_inputHeight" format="dimension"/>
<attr name="vcv_inputColor" format="color"/>
<attr name="vcv_textSize" format="dimension"/>
<attr name="vcv_textColor" format="color"/>
</declare-styleable>
<declare-styleable name="QuoteView">
<attr name="message_type" format="enum">
<enum name="preview" value="0" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="registration_headline_text">18sp</dimen>
<dimen name="registration_body_text">12sp</dimen>
<dimen name="registration_button_bottom_margin">32dp</dimen>
</resources>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Button.Registration" parent="Button.Borderless">
<item name="android:textSize">@dimen/registration_body_text</item>
<item name="android:textColor">@color/white</item>
<item name="android:elevation" tools:ignore="NewApi">4dp</item>
<item name="cpb_cornerRadius">4dp</item>
<item name="cpb_colorIndicator">@color/white</item>
<item name="cpb_colorProgress">@color/textsecure_primary</item>
<item name="cpb_selectorIdle">@drawable/progress_button_state</item>
</style>
<style name="Signal.Text.Headline.Registration">
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="Signal.Text.Body.Registration">
<item name="android:textSize">@dimen/registration_body_text</item>
</style>
<style name="Button.Borderless.Registration">
<item name="android:textColor">@color/core_grey_60</item>
</style>
</resources>

View File

@ -669,6 +669,9 @@
<string name="RegistrationActivity_enter_your_phone_number_to_get_started">Enter your phone number to get started</string>
<string name="RegistrationActivity_you_will_receive_a_verification_code">You will receive a verification code. Carrier rates may apply.</string>
<string name="RegistrationActivity_enter_the_code_we_sent_to_s">Enter the code we sent to %s</string>
<string name="RegistrationActivity_phone_number_description">Phone number</string>
<string name="RegistrationActivity_country_code_description">Country code</string>
<string name="RegistrationActivity_call">Call</string>
<!-- RevealableMessageView -->
@ -1581,6 +1584,10 @@
<string name="RegistrationActivity_please_enter_the_verification_code_sent_to_s">Please enter the verification code sent to %s.</string>
<string name="RegistrationActivity_wrong_number">Wrong number</string>
<string name="RegistrationActivity_call_me_instead_available_in">Call me instead \n (Available in %1$02d:%2$02d)</string>
<string name="RegistrationActivity_contact_signal_support">Contact Signal Support</string>
<string name="RegistrationActivity_support_email" translatable="false">support@signal.org</string>
<string name="RegistrationActivity_code_support_subject">Signal Registration - Verification Code for Android</string>
<string name="RegistrationActivity_code_support_body">Subject: Signal Registration - Verification Code for Android\nDevice info: %1$s\nAndroid version: %2$s\nSignal version: %3$s\nLocale: %4$s</string>
<string name="BackupUtil_never">Never</string>
<string name="BackupUtil_unknown">Unknown</string>
<string name="preferences_app_protection__screen_lock">Screen lock</string>

View File

@ -248,10 +248,6 @@
<item name="android:textColor">@color/signal_primary</item>
</style>
<style name="Button.Borderless.Registration" parent="Base.Widget.AppCompat.Button.Borderless">
<item name="android:textColor">@color/core_grey_60</item>
</style>
<!-- RedPhone -->
<!-- Buttons in the main "button row" of the in-call onscreen touch UI. -->

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Signal.Text.Headline" parent="Base.TextAppearance.AppCompat.Headline">
<item name="android:textSize">28sp</item>
<item name="android:lineSpacingExtra">3sp</item>
@ -7,10 +8,6 @@
<item name="android:letterSpacing" tools:ignore="NewApi">0</item>
</style>
<style name="Signal.Text.Headline.Registration" parent="Signal.Text.Headline">
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="Signal.Text.Body" parent="Base.TextAppearance.AppCompat.Body1">
<item name="android:textSize">16sp</item>
<item name="android:lineSpacingExtra">2sp</item>

View File

@ -1,27 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
public class CountrySelectionActivity extends BaseActivity
implements CountrySelectionFragment.CountrySelectedListener
{
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.setContentView(R.layout.country_selection);
}
@Override
public void countrySelected(String countryName, int countryCode) {
Intent result = getIntent();
result.putExtra("country_name", countryName);
result.putExtra("country_code", countryCode);
this.setResult(RESULT_OK, result);
this.finish();
}
}

View File

@ -5,20 +5,20 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.registration.WelcomeActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -29,13 +29,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
public static final String LOCALE_EXTRA = "locale_extra";
private static final int STATE_NORMAL = 0;
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
private static final int STATE_PROMPT_PUSH_REGISTRATION = 4;
private static final int STATE_EXPERIENCE_UPGRADE = 5;
private static final int STATE_WELCOME_SCREEN = 6;
private static final int STATE_NORMAL = 0;
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
private static final int STATE_EXPERIENCE_UPGRADE = 4;
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@ -133,13 +132,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
Log.i(TAG, "routeApplicationState(), state: " + state);
switch (state) {
case STATE_CREATE_PASSPHRASE: return getCreatePassphraseIntent();
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
case STATE_WELCOME_SCREEN: return getWelcomeIntent();
case STATE_PROMPT_PUSH_REGISTRATION: return getPushRegistrationIntent();
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
default: return null;
case STATE_CREATE_PASSPHRASE: return getCreatePassphraseIntent();
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
default: return null;
}
}
@ -150,10 +148,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return STATE_PROMPT_PASSPHRASE;
} else if (ApplicationMigrations.isUpdate(this) && ApplicationMigrations.isUiBlockingMigrationRunning()) {
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasSeenWelcomeScreen(this)) {
return STATE_WELCOME_SCREEN;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_PROMPT_PUSH_REGISTRATION;
return STATE_WELCOME_PUSH_SCREEN;
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
return STATE_EXPERIENCE_UPGRADE;
} else {
@ -180,16 +176,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
}
private Intent getWelcomeIntent() {
return getRoutedIntent(WelcomeActivity.class, getPushRegistrationIntent());
}
private Intent getPushRegistrationIntent() {
return getRoutedIntent(RegistrationActivity.class, getCreateProfileIntent());
}
private Intent getCreateProfileIntent() {
return getRoutedIntent(CreateProfileActivity.class, getConversationListIntent());
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {

File diff suppressed because it is too large Load Diff

View File

@ -84,4 +84,10 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
border.setBackgroundResource(hasFocus ? R.drawable.labeled_edit_text_background_active
: R.drawable.labeled_edit_text_background_inactive);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
input.setEnabled(enabled);
}
}

View File

@ -1,17 +1,18 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButton {
private int countDown;
@Nullable
private Listener listener;
public CallMeCountDownView(Context context) {
super(context);
@ -44,12 +45,25 @@ public class CallMeCountDownView extends androidx.appcompat.widget.AppCompatButt
countDown--;
int minutesRemaining = countDown / 60;
int secondsRemaining = countDown - (minutesRemaining * 60);
int secondsRemaining = countDown % 60;
setText(getResources().getString(R.string.RegistrationActivity_call_me_instead_available_in, minutesRemaining, secondsRemaining));
if (listener != null) {
listener.onRemaining(this, countDown);
}
postDelayed(this::updateCountDown, 1000);
} else if (countDown == 0) {
setCallEnabled();
}
}
public void setListener(@Nullable Listener listener) {
this.listener = listener;
}
public interface Listener {
void onRemaining(@NonNull CallMeCountDownView view, int remaining);
}
}

View File

@ -1,16 +1,8 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
@ -20,6 +12,11 @@ import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
@ -28,66 +25,51 @@ import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.List;
public class VerificationCodeView extends FrameLayout {
public final class VerificationCodeView extends FrameLayout {
private final List<TextView> codes = new ArrayList<>(6);
private final List<View> containers = new ArrayList<>(6);
private OnCodeEnteredListener listener;
private int index = 0;
private int index;
public VerificationCodeView(Context context) {
super(context);
initialize(context, null);
initialize(context);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
initialize(context);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs);
initialize(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context, attrs);
initialize(context);
}
private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
private void initialize(@NonNull Context context) {
inflate(context, R.layout.verification_code_view, this);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerificationCodeView);
codes.add(findViewById(R.id.code_zero));
codes.add(findViewById(R.id.code_one));
codes.add(findViewById(R.id.code_two));
codes.add(findViewById(R.id.code_three));
codes.add(findViewById(R.id.code_four));
codes.add(findViewById(R.id.code_five));
try {
TextView separator = findViewById(R.id.separator);
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.container_three));
this.containers.add(findViewById(R.id.container_four));
this.containers.add(findViewById(R.id.container_five));
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)));
separator.setTextSize(TypedValue.COMPLEX_UNIT_SP, typedArray.getDimension(R.styleable.VerificationCodeView_vcv_textSize, 30));
separator.setTextColor(typedArray.getColor(R.styleable.VerificationCodeView_vcv_textColor, Color.GRAY));
} finally {
if (typedArray != null) typedArray.recycle();
}
containers.add(findViewById(R.id.container_zero));
containers.add(findViewById(R.id.container_one));
containers.add(findViewById(R.id.container_two));
containers.add(findViewById(R.id.container_three));
containers.add(findViewById(R.id.container_four));
containers.add(findViewById(R.id.container_five));
}
@MainThread
@ -143,11 +125,11 @@ public class VerificationCodeView extends FrameLayout {
setInactive(containers);
}
private void setInactive(List<View> views) {
private static void setInactive(List<View> views) {
Stream.of(views).forEach(c -> c.setBackgroundResource(R.drawable.labeled_edit_text_background_inactive));
}
private void setActive(@NonNull View container) {
private static void setActive(@NonNull View container) {
container.setBackgroundResource(R.drawable.labeled_edit_text_background_active);
}

View File

@ -1,11 +1,9 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.content.Intent;
import android.view.View.OnClickListener;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PushRegistrationReminder extends Reminder {
@ -14,13 +12,7 @@ public class PushRegistrationReminder extends Reminder {
super(context.getString(R.string.reminder_header_push_title),
context.getString(R.string.reminder_header_push_text));
final OnClickListener okListener = v -> {
Intent intent = new Intent(context, RegistrationActivity.class);
intent.putExtra(RegistrationActivity.RE_REGISTRATION_EXTRA, true);
context.startActivity(intent);
};
setOkListener(okListener);
setOkListener(v -> context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context)));
}
@Override

View File

@ -1,11 +1,9 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.content.Intent;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class UnauthorizedReminder extends Reminder {
@ -15,9 +13,7 @@ public class UnauthorizedReminder extends Reminder {
context.getString(R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device));
setOkListener(v -> {
Intent intent = new Intent(context, RegistrationActivity.class);
intent.putExtra(RegistrationActivity.RE_REGISTRATION_EXTRA, true);
context.startActivity(intent);
context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context));
});
}

View File

@ -93,7 +93,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.PromptMmsActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
@ -193,6 +192,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
@ -929,9 +929,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleRegisterForSignal() {
Intent intent = new Intent(this, RegistrationActivity.class);
intent.putExtra(RegistrationActivity.RE_REGISTRATION_EXTRA, true);
startActivity(intent);
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(this));
}
private void handleInviteLink() {

View File

@ -9,23 +9,24 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
import org.thoughtcrime.securesms.logging.Log;
import android.widget.Toast;
import com.google.firebase.iid.FirebaseInstanceId;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.LogSubmitActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RegistrationActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
@ -220,12 +221,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
});
builder.show();
} else {
Intent nextIntent = new Intent(getActivity(), ApplicationPreferencesActivity.class);
Intent intent = new Intent(getActivity(), RegistrationActivity.class);
intent.putExtra(RegistrationActivity.RE_REGISTRATION_EXTRA, true);
intent.putExtra("next_intent", nextIntent);
startActivity(intent);
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
}
return false;

View File

@ -1,68 +0,0 @@
package org.thoughtcrime.securesms.registration;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
public class CaptchaActivity extends BaseActionBarActivity {
public static final String KEY_TOKEN = "token";
public static final String KEY_IS_SMS = "is_sms";
private static final String SIGNAL_SCHEME = "signalcaptcha://";
public static Intent getIntent(@NonNull Context context, boolean isSms) {
Intent intent = new Intent(context, CaptchaActivity.class);
intent.putExtra(KEY_IS_SMS, isSms);
return intent;
}
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.captcha_activity);
WebView webView = findViewById(R.id.registration_captcha_web_view);
webView.getSettings().setJavaScriptEnabled(true);
webView.clearCache(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(SIGNAL_SCHEME)) {
handleToken(url.substring(SIGNAL_SCHEME.length()));
return true;
}
return false;
}
});
webView.loadUrl("https://signalcaptchas.org/registration/generate.html");
}
public void handleToken(String token) {
if (!TextUtils.isEmpty(token)) {
Intent result = new Intent();
result.putExtra(KEY_TOKEN, token);
result.putExtra(KEY_IS_SMS, getIntent().getBooleanExtra(KEY_IS_SMS, true));
setResult(RESULT_OK, result);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
}

View File

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

View File

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

View File

@ -1,56 +0,0 @@
package org.thoughtcrime.securesms.registration;
import android.Manifest;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class WelcomeActivity extends BaseActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.registration_welcome_activity);
findViewById(R.id.welcome_terms_button).setOnClickListener(v -> onTermsClicked());
findViewById(R.id.welcome_continue_button).setOnClickListener(v -> onContinueClicked());
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void onTermsClicked() {
CommunicationActions.openBrowserLink(this, "https://signal.org/legal");
}
private void onContinueClicked() {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE)
.ifNecessary()
.withRationaleDialog(getString(R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends),
R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp)
.onAnyResult(() -> {
TextSecurePreferences.setHasSeenWelcomeScreen(WelcomeActivity.this, true);
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
if (nextIntent == null) {
throw new IllegalStateException("Was not supplied a next_intent.");
}
startActivity(nextIntent);
overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
finish();
})
.execute();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,149 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import java.util.concurrent.TimeUnit;
public final class RegistrationLockFragment extends BaseRegistrationFragment {
private EditText pinEntry;
private CircularProgressButton pinButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_lock, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.verify_header));
pinEntry = view.findViewById(R.id.pin);
pinButton = view.findViewById(R.id.pinButton);
View clarificationLabel = view.findViewById(R.id.clarification_label);
View subHeader = view.findViewById(R.id.verify_subheader);
View pinForgotButton = view.findViewById(R.id.forgot_button);
String code = getModel().getTextCodeEntered();
long timeRemaining = RegistrationLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
pinEntry.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
boolean matchesTextCode = s != null && s.toString().equals(code);
clarificationLabel.setVisibility(matchesTextCode ? View.VISIBLE : View.INVISIBLE);
subHeader.setVisibility(matchesTextCode ? View.INVISIBLE : View.VISIBLE);
}
});
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
return false;
});
pinButton.setOnClickListener((v) -> {
hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
}
private void handlePinEntry() {
final String pin = pinEntry.getText().toString();
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
return;
}
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
setSpinning(pinButton);
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), model.getTextCodeEntered(), pin, new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
cancelSpinning(pinButton);
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
}
@Override
public void onIncorrectRegistrationLockPin(long timeRemaining) {
cancelSpinning(pinButton);
pinEntry.setText("");
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_registration_lock_pin, Toast.LENGTH_LONG).show();
}
@Override
public void onTooManyAttempts() {
cancelSpinning(pinButton);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
@Override
public void onError() {
cancelSpinning(pinButton);
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
});
}
private void handleForgottenPin(long timeRemainingMs) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_oh_no)
.setMessage(getString(R.string.RegistrationActivity_registration_of_this_phone_number_will_be_possible_without_your_registration_lock_pin_after_seven_days_have_passed, (TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
}

View File

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

View File

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

View File

@ -0,0 +1,181 @@
package org.thoughtcrime.securesms.registration.service;
import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
import java.util.List;
public final class CodeVerificationRequest {
private static final String TAG = Log.tag(CodeVerificationRequest.class);
private enum Result {
SUCCESS,
PIN_LOCKED,
RATE_LIMITED,
ERROR
}
/**
* Asynchronously verify the account via the code.
*
* @param fcmToken The FCM token for the device.
* @param code The code that was delivered to the user.
* @param pin The users registration pin.
* @param callback Exactly one method on this callback will be called.
*/
static void verifyAccount(@NonNull Context context,
@NonNull Credentials credentials,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@NonNull VerifyCallback callback)
{
new AsyncTask<Void, Void, Result>() {
private volatile long timeRemaining;
@Override
protected Result doInBackground(Void... voids) {
try {
verifyAccount(context, credentials, code, pin, fcmToken);
return Result.SUCCESS;
} catch (LockedException e) {
Log.w(TAG, e);
timeRemaining = e.getTimeRemaining();
return Result.PIN_LOCKED;
} catch (RateLimitException e) {
Log.w(TAG, e);
return Result.RATE_LIMITED;
} catch (IOException e) {
Log.w(TAG, e);
return Result.ERROR;
}
}
@Override
protected void onPostExecute(Result result) {
if (result == Result.SUCCESS) {
handleSuccessfulRegistration(context, pin);
callback.onSuccessfulRegistration();
} else if (result == Result.PIN_LOCKED) {
callback.onIncorrectRegistrationLockPin(timeRemaining);
} else if (result == Result.RATE_LIMITED) {
callback.onTooManyAttempts();
} else if (result == Result.ERROR) {
callback.onError();
}
}
}.execute();
}
private static void handleSuccessfulRegistration(@NonNull Context context, @Nullable String pin) {
TextSecurePreferences.setRegistrationLockPin(context, pin);
TextSecurePreferences.setRegistrationtLockEnabled(context, pin != null);
if (pin != null) {
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
}
JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.add(new DirectoryRefreshJob(false));
jobManager.add(new RotateCertificateJob(context));
DirectoryRefreshListener.schedule(context);
RotateSignedPreKeyListener.schedule(context);
}
private static void verifyAccount(@NonNull Context context, @NonNull Credentials credentials, @NonNull String code, @Nullable String pin, @Nullable String fcmToken) throws IOException {
int registrationId = KeyHelper.generateRegistrationId(false);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(context);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
SessionUtil.archiveAllSessions(context);
SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context, credentials.getE164number(), credentials.getPassword());
boolean present = fcmToken != null;
accountManager.verifyAccountWithCode(code, null, registrationId, !present, pin,
unidentifiedAccessKey, universalUnidentifiedAccess);
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (present) {
accountManager.setGcmId(Optional.fromNullable(fcmToken));
}
TextSecurePreferences.setFcmToken(context, fcmToken);
TextSecurePreferences.setFcmDisabled(context, !present);
TextSecurePreferences.setWebsocketRegistered(context, true);
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(Recipient.self().getId(),
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setVerifying(context, false);
TextSecurePreferences.setPushRegistered(context, true);
TextSecurePreferences.setPushServerPassword(context, credentials.getPassword());
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.self().getId(), true);
DatabaseFactory.getRecipientDatabase(context).setRegistered(Recipient.self().getId(), RecipientDatabase.RegisteredState.REGISTERED);
}
public interface VerifyCallback {
void onSuccessfulRegistration();
/**
* @param timeRemaining Time until pin expires and number can be reused.
*/
void onIncorrectRegistrationLockPin(long timeRemaining);
void onTooManyAttempts();
void onError();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.util.Util;
public final class RegistrationViewModel extends ViewModel {
private final String secret;
private final MutableLiveData<NumberViewState> number;
private final MutableLiveData<String> textCodeEntered;
private final MutableLiveData<String> captchaToken;
private final MutableLiveData<String> fcmToken;
private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL);
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
}
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
if (!savedStateHandle.contains(key)) {
savedStateHandle.set(key, initialValue);
}
return savedStateHandle.get(key);
}
public @NonNull NumberViewState getNumber() {
//noinspection ConstantConditions Live data was given an initial value
return number.getValue();
}
public @NonNull LiveData<NumberViewState> getLiveNumber() {
return number;
}
public @NonNull String getTextCodeEntered() {
//noinspection ConstantConditions Live data was given an initial value
return textCodeEntered.getValue();
}
public String getCaptchaToken() {
return captchaToken.getValue();
}
public boolean hasCaptchaToken() {
return getCaptchaToken() != null;
}
public String getRegistrationSecret() {
return secret;
}
public void onCaptchaResponse(String captchaToken) {
this.captchaToken.setValue(captchaToken);
}
public void clearCaptchaResponse() {
captchaToken.setValue(null);
}
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
setViewState(getNumber().toBuilder()
.selectedCountryDisplayName(selectedCountryName)
.countryCode(countryCode).build());
}
public void setNationalNumber(long number) {
NumberViewState numberViewState = getNumber().toBuilder().nationalNumber(number).build();
setViewState(numberViewState);
}
private void setViewState(NumberViewState numberViewState) {
if (!numberViewState.equals(getNumber())) {
number.setValue(numberViewState);
}
}
@MainThread
public void onVerificationCodeEntered(String code) {
textCodeEntered.setValue(code);
}
public void onNumberDetected(int countryCode, long nationalNumber) {
setViewState(getNumber().toBuilder()
.countryCode(countryCode)
.nationalNumber(nationalNumber)
.build());
}
public String getFcmToken() {
return fcmToken.getValue();
}
@MainThread
public void setFcmToken(@Nullable String fcmToken) {
this.fcmToken.setValue(fcmToken);
}
public void setWelcomeSkippedOnRestore() {
restoreFlowShown.setValue(true);
}
public boolean hasRestoreFlowBeenShown() {
//noinspection ConstantConditions Live data was given an initial value
return restoreFlowShown.getValue();
}
public void markASuccessfulAttempt() {
//noinspection ConstantConditions Live data was given an initial value
successfulCodeRequestAttempts.setValue(successfulCodeRequestAttempts.getValue() + 1);
}
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
return successfulCodeRequestAttempts;
}
}

View File

@ -2,12 +2,12 @@
dependencyVerification {
verify = [
'androidx.activity:activity:0d6bafb56a72da893f3990ca5d819214d047f5f6b5c5f822ed97971c05eeb85a',
'androidx.activity:activity:d1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d',
'androidx.annotation:annotation:d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692',
'androidx.appcompat:appcompat-resources:53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1',
'androidx.appcompat:appcompat:49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd',
'androidx.arch.core:core-common:fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889',
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
'androidx.arch.core:core-runtime:dd77615bd3dd275afb11b62df25bae46b10b4a117cd37943af45bdcbf8755852',
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
'androidx.camera:camera-camera2:b7897230aec96365d675712c92f5edcb8b464badfd61788c8f956ec2d6e49bfe',
'androidx.camera:camera-core:e1c70de55600a0caf826eb4f8a75c96c5ff8f0b626bf08413d31e80ffa55f8ba',
@ -17,13 +17,13 @@ dependencyVerification {
'androidx.constraintlayout:constraintlayout-solver:965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045',
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
'androidx.coordinatorlayout:coordinatorlayout:e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73',
'androidx.core:core:45c7a50ad1f366e62db496d8cef7730d5ee1681215007d1a19e6b6d800a12842',
'androidx.core:core:76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e',
'androidx.cursoradapter:cursoradapter:a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564',
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
'androidx.fragment:fragment:9656d81c472b5142bbc3471ef7259fbc93905dc38e823c63a99e48819881b6e7',
'androidx.fragment:fragment:a14c8b8f2153f128e800fbd266a6beab1c283982a29ec570d2cc05d307d81496',
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
'androidx.legacy:legacy-preference-v14:d6d11913e56b8f2d14fd560bd1ad6d7fd5624a15dd4ec073b2d9188205f86280',
@ -31,29 +31,34 @@ dependencyVerification {
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
'androidx.lifecycle:lifecycle-common-java8:9edc2d4f589656d470ef03b9c6ece62d335971294b033ec7d9ceb6e361e9aafa',
'androidx.lifecycle:lifecycle-common:76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643',
'androidx.lifecycle:lifecycle-extensions:8d4072201b6231d67e4192d608d46b1f5c920845106c9831632c2e3ffe706117',
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
'androidx.lifecycle:lifecycle-livedata:c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39',
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
'androidx.lifecycle:lifecycle-runtime:7e6d414d03bb184f3015dacc6233eeaded45fa23f0cf4c1f6d3395d6495fa41c',
'androidx.lifecycle:lifecycle-service:cb2b15bb0cf14134e953ed8ead96f94265018643f519367d51fd837f7311e9f8',
'androidx.lifecycle:lifecycle-viewmodel:9f2efb59328027fa9f0c413d4d5910aab68d149b139ca8ce432135105b74833a',
'androidx.lifecycle:lifecycle-common-java8:a1ec63c1bb973443cb731d78ec336c5e20e7ee35c89cbb32d36f92c55bb02542',
'androidx.lifecycle:lifecycle-common:63898dabf7cfe5ec5d7ed8b8c2564c1427be876e1496ead95c2703cf59d3734b',
'androidx.lifecycle:lifecycle-extensions:bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d',
'androidx.lifecycle:lifecycle-livedata-core:6df2bcbf3be50a5fa29e9aa09d39437f2d61d17c2c46ef618d65bbac4a4a99fc',
'androidx.lifecycle:lifecycle-livedata:242e446bed3db36f0df0aab0cb7f91060bd2dab7adcad1117adf54e724cd1d26',
'androidx.lifecycle:lifecycle-process:8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac',
'androidx.lifecycle:lifecycle-runtime:e5173897b965e870651e83d9d5af1742d3f532d58863223a390ce3a194c8312b',
'androidx.lifecycle:lifecycle-service:23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69',
'androidx.lifecycle:lifecycle-viewmodel-savedstate:f503b53f50c4e6c1f9a3d698c4733df6e7a44049fe477ad0b85cc2f460401fbc',
'androidx.lifecycle:lifecycle-viewmodel:7725715491963440ee483e46526cd4f83af1c758e072e97b3eab2115c6f4db35',
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
'androidx.navigation:navigation-common:f968fcaa2fd94b0d1275ce175ecfb4773678732ead9b4d81993ffd5bc3fe3c7c',
'androidx.navigation:navigation-fragment:776ba1be826f8de7cb262f55ece262c5eb9947758cbd2e902298750521404dd2',
'androidx.navigation:navigation-runtime:499029c016345a2a2130ee7a32670871757e5fc7e6d1b93be8962bb59fa5ce9d',
'androidx.navigation:navigation-ui:1ec0558d692982c5bcfcca6de5b5972723e6b4a9870aa7fc1eddf5e869f116ed',
'androidx.preference:preference:ea9fde25606eb456210ffe9f7e51048abd776b55a34c0cc6608282b5699122d1',
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
'androidx.savedstate:savedstate:115ac7313095b2d159565d2bc851a7722e43fc00347fc828214ff8917799b5f0',
'androidx.savedstate:savedstate:2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83',
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
'androidx.transition:transition:c374bef04f01580ba76447e759ea560079727779ff882ad55735fd445edca8b4',
'androidx.vectordrawable:vectordrawable-animated:f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f',
'androidx.vectordrawable:vectordrawable:b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26',
'androidx.versionedparcelable:versionedparcelable:948c751f6352d4c0f93f15fa1bf506c59083bc7754264dd9a325a6da0e2eec05',
'androidx.versionedparcelable:versionedparcelable:9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1',
'androidx.viewpager:viewpager:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682',
'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729',
'com.airbnb.android:lottie:6819ff968eb768096133c7873d63351705fd4ac424a0917d86c4145f5035097d',