Registration refactor initial scaffolding.
This commit is contained in:
parent
318b59a6b2
commit
eec2685e67
31 changed files with 2732 additions and 228 deletions
|
@ -837,6 +837,13 @@
|
|||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registration.v2.ui.RegistrationV2Activity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
|
|
|
@ -28,8 +28,10 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
|
|||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
@ -208,7 +210,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
if (FeatureFlags.registrationV2()) {
|
||||
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
|
||||
} else {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
|
|
|
@ -59,7 +59,7 @@ class VerificationCodeView @JvmOverloads constructor(context: Context, attrs: At
|
|||
}
|
||||
}
|
||||
|
||||
interface OnCodeEnteredListener {
|
||||
fun interface OnCodeEnteredListener {
|
||||
fun onCodeComplete(code: String)
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ public final class PushChallengeRequest {
|
|||
}
|
||||
}
|
||||
|
||||
static class PushChallengeEvent {
|
||||
public static class PushChallengeEvent {
|
||||
private final String challenge;
|
||||
|
||||
PushChallengeEvent(String challenge) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.registration
|
|||
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
|
||||
// TODO [regv2]: fold sessionId into this?
|
||||
data class RegistrationData(
|
||||
val code: String,
|
||||
val e164: String,
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GrantPermissionsScreenPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
GrantPermissionsScreen(
|
||||
deviceBuildVersion = 33,
|
||||
isBackupSelectionRequired = true,
|
||||
isSearchingForBackup = true,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout that explains permissions rationale to the user.
|
||||
*/
|
||||
@Composable
|
||||
fun GrantPermissionsScreen(
|
||||
deviceBuildVersion: Int,
|
||||
isBackupSelectionRequired: Boolean,
|
||||
isSearchingForBackup: Boolean,
|
||||
onNextClicked: () -> Unit,
|
||||
onNotNowClicked: () -> Unit
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__allow_permissions),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__to_help_you_message_people_you_know),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 41.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (deviceBuildVersion >= 33) {
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_contact),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__contacts),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__find_people_you_know)
|
||||
)
|
||||
}
|
||||
|
||||
if (deviceBuildVersion < 29 || !isBackupSelectionRequired) {
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_file),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__storage),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__send_photos_videos_and_files)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_phone),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__phone_calls),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__make_registering_easier)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
TextButton(onClick = onNotNowClicked) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__not_now)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (isSearchingForBackup) {
|
||||
Box {
|
||||
NextButton(
|
||||
isSearchingForBackup = true,
|
||||
onNextClicked = onNextClicked
|
||||
)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
NextButton(
|
||||
isSearchingForBackup = false,
|
||||
onNextClicked = onNextClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PermissionRowPreview() {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionRow(
|
||||
imageVector: ImageVector,
|
||||
title: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Row(modifier = Modifier.padding(bottom = 32.dp)) {
|
||||
Image(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NextButton(
|
||||
isSearchingForBackup: Boolean,
|
||||
onNextClicked: () -> Unit
|
||||
) {
|
||||
val alpha = if (isSearchingForBackup) {
|
||||
0f
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClicked,
|
||||
enabled = !isSearchingForBackup,
|
||||
modifier = Modifier.alpha(alpha)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__next)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import android.view.MenuInflater;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
|
@ -138,7 +137,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
|||
}
|
||||
controller.setNumberAndCountryCode(viewModelNumber);
|
||||
|
||||
showKeyboard(number.getEditText());
|
||||
ViewUtil.focusAndShowKeyboard(number.getEditText());
|
||||
|
||||
if (viewModel.hasUserSkippedReRegisterFlow() && viewModel.shouldAutoShowSmsConfirmDialog()) {
|
||||
viewModel.setAutoShowSmsConfirmDialog(false);
|
||||
|
@ -146,12 +145,6 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R
|
|||
}
|
||||
}
|
||||
|
||||
private void showKeyboard(View viewToFocus) {
|
||||
viewToFocus.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(viewToFocus, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.enter_phone_number, menu);
|
||||
|
|
|
@ -6,38 +6,15 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
|
||||
|
@ -124,180 +101,3 @@ class GrantPermissionsFragment : ComposeFragment() {
|
|||
RESTORE_BACKUP
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun GrantPermissionsScreenPreview() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
GrantPermissionsScreen(
|
||||
deviceBuildVersion = 33,
|
||||
isBackupSelectionRequired = true,
|
||||
isSearchingForBackup = true,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GrantPermissionsScreen(
|
||||
deviceBuildVersion: Int,
|
||||
isBackupSelectionRequired: Boolean,
|
||||
isSearchingForBackup: Boolean,
|
||||
onNextClicked: () -> Unit,
|
||||
onNotNowClicked: () -> Unit
|
||||
) {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__allow_permissions),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__to_help_you_message_people_you_know),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 41.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (deviceBuildVersion >= 33) {
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_contact),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__contacts),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__find_people_you_know)
|
||||
)
|
||||
}
|
||||
|
||||
if (deviceBuildVersion < 29 || !isBackupSelectionRequired) {
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_file),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__storage),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__send_photos_videos_and_files)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_phone),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__phone_calls),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__make_registering_easier)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
TextButton(onClick = onNotNowClicked) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__not_now)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (isSearchingForBackup) {
|
||||
Box {
|
||||
NextButton(
|
||||
isSearchingForBackup = true,
|
||||
onNextClicked = onNextClicked
|
||||
)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
NextButton(
|
||||
isSearchingForBackup = false,
|
||||
onNextClicked = onNextClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PermissionRowPreview() {
|
||||
PermissionRow(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
|
||||
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
|
||||
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PermissionRow(
|
||||
imageVector: ImageVector,
|
||||
title: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Row(modifier = Modifier.padding(bottom = 32.dp)) {
|
||||
Image(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NextButton(
|
||||
isSearchingForBackup: Boolean,
|
||||
onNextClicked: () -> Unit
|
||||
) {
|
||||
val alpha = if (isSearchingForBackup) {
|
||||
0f
|
||||
} else {
|
||||
1f
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClicked,
|
||||
enabled = !isSearchingForBackup,
|
||||
modifier = Modifier.alpha(alpha)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.GrantPermissionsFragment__next)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -14,7 +19,8 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
|
||||
final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
||||
// TODO [nicholas]: move to v2 package and make package-private. convert to Kotlin
|
||||
public final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
||||
implements DefaultLifecycleObserver
|
||||
{
|
||||
private static final String TAG = Log.tag(SignalStrengthPhoneStateListener.class);
|
||||
|
@ -22,7 +28,8 @@ final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
|||
private final Callback callback;
|
||||
private final Debouncer debouncer = new Debouncer(1000);
|
||||
|
||||
SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) {
|
||||
@SuppressWarnings("deprecation")
|
||||
public SignalStrengthPhoneStateListener(@NonNull LifecycleOwner lifecycleOwner, @NonNull Callback callback) {
|
||||
this.callback = callback;
|
||||
|
||||
lifecycleOwner.getLifecycle().addObserver(this);
|
||||
|
@ -51,7 +58,7 @@ final class SignalStrengthPhoneStateListener extends PhoneStateListener
|
|||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
public interface Callback {
|
||||
void onNoCellSignalPresent();
|
||||
|
||||
void onCellSignalPresent();
|
||||
|
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.AppCapabilities
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.gcm.FcmUtil
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.pin.SvrRepository.onRegistrationComplete
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* A repository that deals with disk I/O during account registration.
|
||||
*/
|
||||
object RegistrationRepository {
|
||||
|
||||
private val TAG = Log.tag(RegistrationRepository::class.java)
|
||||
|
||||
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
|
||||
|
||||
/**
|
||||
* Retrieve the FCM token from the Firebase service.
|
||||
*/
|
||||
suspend fun getFcmToken(context: Context): String? =
|
||||
withContext(Dispatchers.Default) {
|
||||
FcmUtil.getToken(context).orElse(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the local store for whether a PIN is set.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasPin(): Boolean {
|
||||
return SignalStore.svr().hasPin()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local registration ID.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getRegistrationId(): Int {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
var registrationId = SignalStore.account().registrationId
|
||||
if (registrationId == 0) {
|
||||
registrationId = KeyHelper.generateRegistrationId(false)
|
||||
SignalStore.account().registrationId = registrationId
|
||||
}
|
||||
return registrationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local PNI registration ID.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPniRegistrationId(): Int {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
var pniRegistrationId = SignalStore.account().pniRegistrationId
|
||||
if (pniRegistrationId == 0) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
SignalStore.account().pniRegistrationId = pniRegistrationId
|
||||
}
|
||||
return pniRegistrationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries, and creates if needed, the local profile key.
|
||||
*/
|
||||
@JvmStatic
|
||||
suspend fun getProfileKey(e164: String): ProfileKey =
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
|
||||
val recipientTable = SignalDatabase.recipients
|
||||
val recipient = recipientTable.getByE164(e164)
|
||||
var profileKey = if (recipient.isPresent) {
|
||||
ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).profileKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (profileKey == null) {
|
||||
profileKey = ProfileKeyUtil.createNew()
|
||||
Log.i(TAG, "No profile key found, created a new one")
|
||||
}
|
||||
profileKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a server response from a successful registration and persists the relevant data.
|
||||
*/
|
||||
@WorkerThread
|
||||
@JvmStatic
|
||||
suspend fun registerAccountLocally(context: Context, registrationData: RegistrationData, response: AccountRegistrationResult, reglockEnabled: Boolean) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val aciPreKeyCollection: PreKeyCollection = response.aciPreKeyCollection
|
||||
val pniPreKeyCollection: PreKeyCollection = response.pniPreKeyCollection
|
||||
val aci: ACI = ACI.parseOrThrow(response.uuid)
|
||||
val pni: PNI = PNI.parseOrThrow(response.pni)
|
||||
val hasPin: Boolean = response.storageCapable
|
||||
|
||||
SignalStore.account().setAci(aci)
|
||||
SignalStore.account().setPni(pni)
|
||||
|
||||
ApplicationDependencies.resetProtocolStores()
|
||||
|
||||
ApplicationDependencies.getProtocolStore().aci().sessions().archiveAllSessions()
|
||||
ApplicationDependencies.getProtocolStore().pni().sessions().archiveAllSessions()
|
||||
SenderKeyUtil.clearAllState()
|
||||
|
||||
val aciProtocolStore = ApplicationDependencies.getProtocolStore().aci()
|
||||
val aciMetadataStore = SignalStore.account().aciPreKeys
|
||||
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
storeSignedAndLastResortPreKeys(aciProtocolStore, aciMetadataStore, aciPreKeyCollection)
|
||||
storeSignedAndLastResortPreKeys(pniProtocolStore, pniMetadataStore, pniPreKeyCollection)
|
||||
|
||||
val recipientTable = SignalDatabase.recipients
|
||||
val selfId = Recipient.trustedPush(aci, pni, registrationData.e164).id
|
||||
|
||||
recipientTable.setProfileSharing(selfId, true)
|
||||
recipientTable.markRegisteredOrThrow(selfId, aci)
|
||||
recipientTable.linkIdsForSelf(aci, pni, registrationData.e164)
|
||||
recipientTable.setProfileKey(selfId, registrationData.profileKey)
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clearSelf()
|
||||
|
||||
SignalStore.account().setE164(registrationData.e164)
|
||||
SignalStore.account().fcmToken = registrationData.fcmToken
|
||||
SignalStore.account().fcmEnabled = registrationData.isFcm
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
|
||||
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
|
||||
|
||||
SignalStore.account().setServicePassword(registrationData.password)
|
||||
SignalStore.account().setRegistered(true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(context, true)
|
||||
TextSecurePreferences.setUnauthorizedReceived(context, false)
|
||||
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
|
||||
|
||||
onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
PreKeysSyncJob.enqueue()
|
||||
|
||||
val jobManager = ApplicationDependencies.getJobManager()
|
||||
jobManager.add(DirectoryRefreshJob(false))
|
||||
jobManager.add(RotateCertificateJob())
|
||||
|
||||
DirectoryRefreshListener.schedule(context)
|
||||
RotateSignedPreKeyListener.schedule(context)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun saveOwnIdentityKey(selfId: RecipientId, serviceId: ServiceId, protocolStore: SignalServiceAccountDataStoreImpl, now: Long) {
|
||||
protocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
selfId,
|
||||
serviceId,
|
||||
protocolStore.identityKeyPair.publicKey,
|
||||
IdentityTable.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
now,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun storeSignedAndLastResortPreKeys(protocolStore: SignalServiceAccountDataStoreImpl, metadataStore: PreKeyMetadataStore, preKeyCollection: PreKeyCollection) {
|
||||
PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.signedPreKey)
|
||||
metadataStore.isSignedPreKeyRegistered = true
|
||||
metadataStore.activeSignedPreKeyId = preKeyCollection.signedPreKey.id
|
||||
metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis()
|
||||
|
||||
PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.lastResortKyberPreKey)
|
||||
metadataStore.lastResortKyberPreKeyId = preKeyCollection.lastResortKyberPreKey.id
|
||||
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
|
||||
* This requires two or more network calls:
|
||||
* 1. Create (or reuse) a session.
|
||||
* 2. (Optional) If the session has any proof requirements ("challenges"), the user must solve them and submit the proof.
|
||||
* 3. Once the service responds we are allowed to, we request the verification code.
|
||||
*/
|
||||
suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
val activeSession = if (fcmToken == null) {
|
||||
// TODO [regv2]
|
||||
val notImplementedError = NotImplementedError()
|
||||
Log.w(TAG, "Not yet implemented!", notImplementedError)
|
||||
NetworkResult.ApplicationError(notImplementedError)
|
||||
} else {
|
||||
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
|
||||
}
|
||||
|
||||
activeSession.then { session ->
|
||||
val sessionId = session.body.id
|
||||
SignalStore.registrationValues().sessionId = sessionId
|
||||
SignalStore.registrationValues().sessionE164 = e164
|
||||
if (!session.body.allowedToRequestCode) {
|
||||
val challenges = session.body.requestedInformation.joinToString()
|
||||
Log.w(TAG, "Not allowed to request code! Remaining challenges: $challenges")
|
||||
// TODO [regv2]: actually handle challenges
|
||||
}
|
||||
// TODO [regv2]: support other verification code [Mode] options
|
||||
if (mode == Mode.PHONE_CALL) {
|
||||
// TODO [regv2]
|
||||
val notImplementedError = NotImplementedError()
|
||||
Log.w(TAG, "Not yet implemented!", notImplementedError)
|
||||
NetworkResult.ApplicationError(notImplementedError)
|
||||
} else {
|
||||
api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the user-entered verification code to the service.
|
||||
*/
|
||||
suspend fun submitVerificationCode(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
api.verifyAccount(registrationData.code, sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the necessary assets as a verified account so that the user can actually use the service.
|
||||
*/
|
||||
suspend fun registerAccount(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
|
||||
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
val masterKey: MasterKey? = masterKeyProducer?.produceMasterKey()
|
||||
val registrationLock: String? = masterKey?.deriveRegistrationLock()
|
||||
|
||||
val accountAttributes = AccountAttributes(
|
||||
signalingKey = null,
|
||||
registrationId = registrationData.registrationId,
|
||||
fetchesMessages = registrationData.isNotFcm,
|
||||
registrationLock = registrationLock,
|
||||
unidentifiedAccessKey = unidentifiedAccessKey,
|
||||
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
|
||||
capabilities = AppCapabilities.getCapabilities(true),
|
||||
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE,
|
||||
name = null,
|
||||
pniRegistrationId = registrationData.pniRegistrationId,
|
||||
recoveryPassword = registrationData.recoveryPassword
|
||||
)
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
val aciIdentity: IdentityKeyPair = SignalStore.account().aciIdentityKey
|
||||
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
val pniIdentity: IdentityKeyPair = SignalStore.account().pniIdentityKey
|
||||
|
||||
val aciPreKeyCollection = org.thoughtcrime.securesms.registration.RegistrationRepository.generateSignedAndLastResortPreKeys(aciIdentity, SignalStore.account().aciPreKeys)
|
||||
val pniPreKeyCollection = org.thoughtcrime.securesms.registration.RegistrationRepository.generateSignedAndLastResortPreKeys(pniIdentity, SignalStore.account().pniPreKeys)
|
||||
|
||||
api.registerAccount(sessionId, registrationData.recoveryPassword, accountAttributes, aciPreKeyCollection, pniPreKeyCollection, registrationData.fcmToken, true)
|
||||
.map { accountRegistrationResponse ->
|
||||
AccountRegistrationResult(
|
||||
uuid = accountRegistrationResponse.uuid,
|
||||
pni = accountRegistrationResponse.pni,
|
||||
storageCapable = accountRegistrationResponse.storageCapable,
|
||||
number = accountRegistrationResponse.number,
|
||||
masterKey = masterKey,
|
||||
pin = pin,
|
||||
aciPreKeyCollection = aciPreKeyCollection,
|
||||
pniPreKeyCollection = pniPreKeyCollection
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> =
|
||||
withContext(Dispatchers.IO) {
|
||||
// TODO [regv2]: do not use event bus nor latch
|
||||
val subscriber = PushTokenChallengeSubscriber()
|
||||
val eventBus = EventBus.getDefault()
|
||||
eventBus.register(subscriber)
|
||||
|
||||
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc).successOrThrow() // TODO: error handling
|
||||
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
eventBus.unregister(subscriber)
|
||||
|
||||
if (receivedPush) {
|
||||
val challenge = subscriber.challenge
|
||||
if (challenge != null) {
|
||||
Log.w(TAG, "Push challenge token received.")
|
||||
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.body.id, challenge)
|
||||
} else {
|
||||
Log.w(TAG, "Push received but challenge token was null.")
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Push challenge timed out.")
|
||||
}
|
||||
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
|
||||
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(NullPointerException())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deriveTimestamp(headers: RegistrationSessionMetadataHeaders, deltaSeconds: Int?): Long {
|
||||
if (deltaSeconds == null) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
val timestamp: Long = headers.timestamp
|
||||
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
|
||||
}
|
||||
|
||||
enum class Mode(val isSmsRetrieverSupported: Boolean) {
|
||||
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
|
||||
}
|
||||
|
||||
private class PushTokenChallengeSubscriber {
|
||||
var challenge: String? = null
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
@Subscribe
|
||||
fun onChallengeEvent(pushChallengeEvent: PushChallengeRequest.PushChallengeEvent) {
|
||||
challenge = pushChallengeEvent.challenge
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
data class AccountRegistrationResult(
|
||||
val uuid: String,
|
||||
val pni: String,
|
||||
val storageCapable: Boolean,
|
||||
val number: String,
|
||||
val masterKey: MasterKey?,
|
||||
val pin: String?,
|
||||
val aciPreKeyCollection: PreKeyCollection,
|
||||
val pniPreKeyCollection: PreKeyCollection
|
||||
)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
|
||||
/**
|
||||
* Activity to hold the entire registration process.
|
||||
*/
|
||||
class RegistrationV2Activity : AppCompatActivity() {
|
||||
|
||||
private val TAG = Log.tag(RegistrationV2Activity::class.java)
|
||||
|
||||
val sharedViewModel: RegistrationV2ViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_registration_navigation_v2)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newIntentForNewRegistration(context: Context, originalIntent: Intent): Intent {
|
||||
return Intent(context, RegistrationV2Activity::class.java).apply {
|
||||
setData(originalIntent.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
|
||||
fun PhoneNumber.toE164(): String {
|
||||
return PhoneNumberUtil.getInstance().format(this, PhoneNumberUtil.PhoneNumberFormat.E164)
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.entercode
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
|
||||
/**
|
||||
* The final screen of account registration, where the user enters their verification code.
|
||||
*/
|
||||
class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) {
|
||||
|
||||
private val TAG = Log.tag(EnterCodeV2Fragment::class.java)
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val binding: FragmentRegistrationEnterCodeV2Binding by ViewBinderDelegate(FragmentRegistrationEnterCodeV2Binding::bind)
|
||||
|
||||
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
|
||||
|
||||
private var autopilotCodeEntryActive = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
|
||||
phoneStateListener = SignalStrengthPhoneStateListener(this, PhoneStateCallback())
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.wrongNumber.setOnClickListener {
|
||||
popBackStack()
|
||||
}
|
||||
|
||||
binding.code.setOnCompleteListener {
|
||||
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
|
||||
}
|
||||
|
||||
binding.keyboard.setOnKeyPressListener { key ->
|
||||
if (!autopilotCodeEntryActive) {
|
||||
if (key >= 0) {
|
||||
binding.code.append(key)
|
||||
} else {
|
||||
binding.code.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
||||
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
|
||||
handleSuccessfulVerify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccessfulVerify() {
|
||||
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
|
||||
val activity = requireActivity()
|
||||
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
|
||||
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
|
||||
val needsProfile = isProfileNameEmpty || isAvatarEmpty
|
||||
val needsPin = !sharedViewModel.hasPin()
|
||||
|
||||
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
|
||||
|
||||
if (!needsProfile && !needsPin) {
|
||||
sharedViewModel.completeRegistration()
|
||||
}
|
||||
|
||||
val startIntent = MainActivity.clearTop(activity).apply {
|
||||
if (needsPin) {
|
||||
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(activity))
|
||||
}
|
||||
|
||||
if (needsProfile) {
|
||||
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(activity))
|
||||
}
|
||||
}
|
||||
|
||||
activity.startActivity(startIntent)
|
||||
sharedViewModel.setInProgress(false)
|
||||
activity.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
|
||||
}
|
||||
|
||||
private fun popBackStack() {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED)
|
||||
NavHostFragment.findNavController(this).popBackStack()
|
||||
}
|
||||
|
||||
private class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
|
||||
override fun onNoCellSignalPresent() {
|
||||
// TODO [regv2]: animate in bottom sheet
|
||||
}
|
||||
|
||||
override fun onCellSignalPresent() {
|
||||
// TODO [regv2]: animate in bottom sheet
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
|
||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2State
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Screen in account registration that provides rationales for the suggested runtime permissions.
|
||||
*/
|
||||
@RequiresApi(23)
|
||||
class GrantPermissionsV2Fragment : ComposeFragment() {
|
||||
|
||||
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
|
||||
private val isSearchingForBackup = mutableStateOf(false)
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions(),
|
||||
::permissionsGranted
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) {
|
||||
if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) {
|
||||
proceedToNextScreen(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedToNextScreen(it: RegistrationV2State) {
|
||||
// TODO [nicholas]: conditionally go to backup flow
|
||||
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val isSearchingForBackup by this.isSearchingForBackup
|
||||
|
||||
GrantPermissionsScreen(
|
||||
deviceBuildVersion = Build.VERSION.SDK_INT,
|
||||
isSearchingForBackup = isSearchingForBackup,
|
||||
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
|
||||
onNextClicked = this::onNextClicked,
|
||||
onNotNowClicked = this::onNotNowClicked
|
||||
)
|
||||
}
|
||||
|
||||
private fun onNextClicked() {
|
||||
when (args.welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> continueNext()
|
||||
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueNext() {
|
||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||
val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)
|
||||
requestPermissionLauncher.launch(requiredPermissions)
|
||||
}
|
||||
|
||||
private fun onNotNowClicked() {
|
||||
when (args.welcomeAction) {
|
||||
WelcomeAction.CONTINUE -> continueNotNow()
|
||||
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueNotNow() {
|
||||
NavHostFragment.findNavController(this).popBackStack()
|
||||
}
|
||||
|
||||
private fun permissionsGranted(permissions: Map<String, Boolean>) {
|
||||
permissions.forEach {
|
||||
Log.d(TAG, "${it.key} = ${it.value}")
|
||||
}
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Which welcome action the user selected which prompted this
|
||||
* screen.
|
||||
*/
|
||||
enum class WelcomeAction {
|
||||
CONTINUE,
|
||||
RESTORE_BACKUP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberV2Binding
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2State
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.toE164
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataObserverCallback
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Screen in registration where the user enters their phone number.
|
||||
*/
|
||||
class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number_v2) {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberV2Fragment::class.java)
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val fragmentViewModel by viewModels<EnterPhoneNumberV2ViewModel>()
|
||||
private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind)
|
||||
|
||||
private lateinit var spinnerAdapter: ArrayAdapter<CountryPrefix>
|
||||
private lateinit var phoneNumberInputLayout: TextInputEditText
|
||||
private lateinit var spinnerView: MaterialAutoCompleteTextView
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
phoneNumberInputLayout = binding.number.editText as TextInputEditText
|
||||
spinnerView = binding.countryCode.editText as MaterialAutoCompleteTextView
|
||||
spinnerAdapter = ArrayAdapter<CountryPrefix>(
|
||||
requireContext(),
|
||||
R.layout.registration_country_code_dropdown_item,
|
||||
fragmentViewModel.supportedCountryPrefixes
|
||||
)
|
||||
setDebugLogSubmitMultiTapView(binding.verifyHeader)
|
||||
binding.registerButton.setOnClickListener { onRegistrationButtonClicked() }
|
||||
|
||||
binding.toolbar.title = null
|
||||
val activity = requireActivity() as AppCompatActivity
|
||||
activity.setSupportActionBar(binding.toolbar)
|
||||
|
||||
requireActivity().addMenuProvider(UseProxyMenuProvider(), viewLifecycleOwner)
|
||||
|
||||
val existingPhoneNumber = sharedViewModel.uiState.value?.phoneNumber
|
||||
if (existingPhoneNumber != null) {
|
||||
fragmentViewModel.restoreState(existingPhoneNumber)
|
||||
fragmentViewModel.phoneNumber()?.let {
|
||||
phoneNumberInputLayout.setText(it.nationalNumber.toString())
|
||||
}
|
||||
} else if (spinnerView.editableText.isBlank()) {
|
||||
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
|
||||
}
|
||||
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
|
||||
presentRegisterButton(sharedState)
|
||||
presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
|
||||
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
|
||||
moveToVerificationEntryScreen()
|
||||
}
|
||||
}
|
||||
|
||||
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
|
||||
if (fragmentViewModel.isEnteredNumberValid(fragmentState)) {
|
||||
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
|
||||
} else {
|
||||
sharedViewModel.setPhoneNumber(null)
|
||||
}
|
||||
|
||||
if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) {
|
||||
presentError(fragmentState)
|
||||
}
|
||||
}
|
||||
|
||||
initializeInputFields()
|
||||
|
||||
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
|
||||
}
|
||||
|
||||
private fun initializeInputFields() {
|
||||
phoneNumberInputLayout.addTextChangedListener {
|
||||
// TODO [regv2]: country code as you type formatter
|
||||
fragmentViewModel.setPhoneNumber(it?.toString())
|
||||
}
|
||||
phoneNumberInputLayout.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
||||
if (hasFocus) {
|
||||
binding.scrollView.postDelayed({ binding.scrollView.smoothScrollTo(0, binding.registerButton.bottom) }, 250)
|
||||
}
|
||||
}
|
||||
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && v != null) {
|
||||
onRegistrationButtonClicked()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
spinnerView.threshold = 100
|
||||
spinnerView.setAdapter(spinnerAdapter)
|
||||
spinnerView.addTextChangedListener { s ->
|
||||
if (s.isNullOrEmpty()) {
|
||||
return@addTextChangedListener
|
||||
}
|
||||
|
||||
if (s[0] != '+') {
|
||||
s.insert(0, "+")
|
||||
}
|
||||
|
||||
fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
|
||||
// TODO [regv2]: setCountryFormatter(it.regionCode)
|
||||
fragmentViewModel.setCountry(it.digits)
|
||||
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
|
||||
phoneNumberInputLayout.setSelection(numberLength, numberLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRegisterButton(sharedState: RegistrationV2State) {
|
||||
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber) && !sharedState.inProgress
|
||||
// TODO [regv2]: always enable the button but display error dialogs if the entered phone number is invalid
|
||||
}
|
||||
|
||||
private fun presentError(state: EnterPhoneNumberV2State) {
|
||||
when (state.error) {
|
||||
EnterPhoneNumberV2State.Error.NONE -> {
|
||||
Unit
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> {
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(getString(R.string.RegistrationActivity_invalid_number))
|
||||
setMessage(
|
||||
String.format(
|
||||
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
|
||||
state.phoneNumber
|
||||
)
|
||||
)
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
|
||||
setOnCancelListener { fragmentViewModel.clearError() }
|
||||
setOnDismissListener { fragmentViewModel.clearError() }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
|
||||
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
|
||||
}
|
||||
|
||||
EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT -> {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRegistrationButtonClicked() {
|
||||
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
|
||||
sharedViewModel.setInProgress(true)
|
||||
val hasFcm = validateFcmStatus(requireContext())
|
||||
if (hasFcm) {
|
||||
sharedViewModel.uiState.observe(viewLifecycleOwner, FcmTokenRetrievedObserver())
|
||||
sharedViewModel.fetchFcmToken(requireContext())
|
||||
} else {
|
||||
sharedViewModel.setInProgress(false)
|
||||
// TODO [regv2]: handle if FCM isn't available
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFcmTokenRetrieved(value: RegistrationV2State) {
|
||||
if (value.phoneNumber == null) {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
|
||||
sharedViewModel.setInProgress(false)
|
||||
} else {
|
||||
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentProgressBar(showProgress: Boolean, isReRegister: Boolean) {
|
||||
if (showProgress) {
|
||||
binding.registerButton.setSpinning()
|
||||
} else {
|
||||
binding.registerButton.cancelSpinning()
|
||||
}
|
||||
binding.countryCode.isEnabled = !showProgress
|
||||
binding.number.isEnabled = !showProgress
|
||||
binding.cancelButton.visible = !showProgress && isReRegister
|
||||
}
|
||||
|
||||
private fun validateFcmStatus(context: Context): Boolean {
|
||||
val fcmStatus = PlayServicesUtil.getPlayServicesStatus(context)
|
||||
Log.d(TAG, "Got $fcmStatus for Play Services status.")
|
||||
when (fcmStatus) {
|
||||
PlayServicesUtil.PlayServicesStatus.SUCCESS -> {
|
||||
return true
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.MISSING -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE)
|
||||
return false
|
||||
}
|
||||
|
||||
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT)
|
||||
return false
|
||||
}
|
||||
|
||||
null -> {
|
||||
Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.")
|
||||
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConfirmNumberDialogCanceled() {
|
||||
Log.d(TAG, "User canceled confirm number, returning to edit number.")
|
||||
sharedViewModel.setInProgress(false)
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout)
|
||||
}
|
||||
|
||||
private fun presentConfirmNumberDialog(phoneNumber: PhoneNumber, isReRegister: Boolean, canSkipSms: Boolean) {
|
||||
val title = if (isReRegister) {
|
||||
R.string.RegistrationActivity_additional_verification_required
|
||||
} else {
|
||||
R.string.RegistrationActivity_phone_number_verification_dialog_title
|
||||
}
|
||||
|
||||
val message: CharSequence = SpannableStringBuilder().apply {
|
||||
append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(phoneNumber.toE164())))
|
||||
if (!canSkipSms) {
|
||||
append("\n\n")
|
||||
append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number))
|
||||
}
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setTitle(title)
|
||||
setMessage(message)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
Log.d(TAG, "User confirmed number.")
|
||||
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
|
||||
}
|
||||
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onConfirmNumberDialogCanceled() }
|
||||
setOnCancelListener { _ -> onConfirmNumberDialogCanceled() }
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun moveToVerificationEntryScreen() {
|
||||
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
|
||||
sharedViewModel.setInProgress(false)
|
||||
}
|
||||
|
||||
private fun popBackStack() {
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.INITIALIZATION)
|
||||
NavHostFragment.findNavController(this).popBackStack()
|
||||
}
|
||||
|
||||
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationV2State>(sharedViewModel.uiState) {
|
||||
override fun onValue(value: RegistrationV2State): Boolean {
|
||||
val fcmRetrieved = value.isFcmSupported
|
||||
if (fcmRetrieved) {
|
||||
onFcmTokenRetrieved(value)
|
||||
}
|
||||
return fcmRetrieved
|
||||
}
|
||||
}
|
||||
|
||||
private inner class UseProxyMenuProvider : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.enter_phone_number, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
|
||||
NavHostFragment.findNavController(this@EnterPhoneNumberV2Fragment).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEditProxy())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
|
||||
/**
|
||||
* State holder for the phone number entry screen, including phone number and Play Services errors.
|
||||
*/
|
||||
data class EnterPhoneNumberV2State(val countryPrefixIndex: Int, val phoneNumber: String, val error: Error = Error.NONE) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val INIT = EnterPhoneNumberV2State(0, "")
|
||||
}
|
||||
|
||||
enum class Error {
|
||||
NONE,
|
||||
INVALID_PHONE_NUMBER,
|
||||
PLAY_SERVICES_MISSING,
|
||||
PLAY_SERVICES_NEEDS_UPDATE,
|
||||
PLAY_SERVICES_TRANSIENT
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.registration.util.CountryPrefix
|
||||
|
||||
/**
|
||||
* ViewModel for the phone number entry screen.
|
||||
*/
|
||||
class EnterPhoneNumberV2ViewModel : ViewModel() {
|
||||
|
||||
private val TAG = Log.tag(EnterPhoneNumberV2ViewModel::class.java)
|
||||
|
||||
private val store = MutableStateFlow(EnterPhoneNumberV2State.INIT)
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
|
||||
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
|
||||
.sortedBy { it.digits.toString() }
|
||||
|
||||
fun countryPrefix(): CountryPrefix {
|
||||
return supportedCountryPrefixes[store.value.countryPrefixIndex]
|
||||
}
|
||||
|
||||
fun phoneNumber(): PhoneNumber? {
|
||||
return try {
|
||||
parsePhoneNumber(store.value)
|
||||
} catch (ex: NumberParseException) {
|
||||
Log.w(TAG, "Could not parse phone number in current state.", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun setPhoneNumber(phoneNumber: String?) {
|
||||
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
|
||||
}
|
||||
|
||||
fun setCountry(digits: Int) {
|
||||
val matchingIndex = countryCodeToAdapterIndex(digits)
|
||||
store.update {
|
||||
it.copy(countryPrefixIndex = matchingIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun parsePhoneNumber(state: EnterPhoneNumberV2State): PhoneNumber {
|
||||
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
|
||||
}
|
||||
|
||||
fun isEnteredNumberValid(state: EnterPhoneNumberV2State): Boolean {
|
||||
return try {
|
||||
PhoneNumberUtil.getInstance().isValidNumber(parsePhoneNumber(state))
|
||||
} catch (ex: NumberParseException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreState(value: PhoneNumber) {
|
||||
val prefixIndex = countryCodeToAdapterIndex(value.countryCode)
|
||||
if (prefixIndex != -1) {
|
||||
store.update {
|
||||
it.copy(
|
||||
countryPrefixIndex = prefixIndex,
|
||||
phoneNumber = value.nationalNumber.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun countryCodeToAdapterIndex(countryCode: Int): Int {
|
||||
return supportedCountryPrefixes.indexOfFirst { prefix -> prefix.digits == countryCode }
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
setError(EnterPhoneNumberV2State.Error.NONE)
|
||||
}
|
||||
|
||||
fun setError(error: EnterPhoneNumberV2State.Error) {
|
||||
store.update {
|
||||
it.copy(error = error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.shared
|
||||
|
||||
/**
|
||||
* An ordered list of checkpoints of the registration process.
|
||||
* This is used for screens to know when to advance, as well as restoring state after process death.
|
||||
*/
|
||||
enum class RegistrationCheckpoint {
|
||||
INITIALIZATION,
|
||||
PERMISSIONS_GRANTED,
|
||||
BACKUP_DETECTED,
|
||||
BACKUP_SELECTED,
|
||||
BACKUP_RESTORED,
|
||||
PUSH_NETWORK_AUDITED,
|
||||
PHONE_NUMBER_CONFIRMED,
|
||||
VERIFICATION_CODE_REQUESTED,
|
||||
CHALLENGE_RECEIVED,
|
||||
CHALLENGE_COMPLETED,
|
||||
VERIFICATION_CODE_ENTERED,
|
||||
VERIFICATION_CODE_VALIDATED,
|
||||
SERVICE_REGISTRATION_COMPLETED,
|
||||
LOCAL_REGISTRATION_COMPLETE
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.shared
|
||||
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
|
||||
/**
|
||||
* State holder shared across all of registration.
|
||||
*/
|
||||
data class RegistrationV2State(
|
||||
val sessionId: String? = null,
|
||||
val phoneNumber: Phonenumber.PhoneNumber? = null,
|
||||
val inProgress: Boolean = false,
|
||||
val isReRegister: Boolean = false,
|
||||
val canSkipSms: Boolean = false,
|
||||
val isFcmSupported: Boolean = false,
|
||||
val fcmToken: String? = null,
|
||||
val nextSms: Long = 0L,
|
||||
val nextCall: Long = 0L,
|
||||
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION
|
||||
)
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.shared
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import com.google.i18n.phonenumbers.Phonenumber
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.toE164
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* ViewModel shared across all of registration.
|
||||
*/
|
||||
class RegistrationV2ViewModel : ViewModel() {
|
||||
|
||||
private val store = MutableStateFlow(RegistrationV2State())
|
||||
|
||||
private val password = Util.getSecret(18) // TODO [regv2]: persist this
|
||||
|
||||
val uiState = store.asLiveData()
|
||||
|
||||
init {
|
||||
val existingE164 = SignalStore.registrationValues().sessionE164
|
||||
if (existingE164 != null) {
|
||||
try {
|
||||
val existingPhoneNumber = PhoneNumberUtil.getInstance().parse(existingE164, null)
|
||||
if (existingPhoneNumber != null) {
|
||||
setPhoneNumber(existingPhoneNumber)
|
||||
}
|
||||
} catch (ex: NumberParseException) {
|
||||
Log.w(TAG, "Could not parse stored E164.", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setInProgress(inProgress: Boolean) {
|
||||
store.update {
|
||||
it.copy(inProgress = inProgress)
|
||||
}
|
||||
}
|
||||
|
||||
fun setRegistrationCheckpoint(checkpoint: RegistrationCheckpoint) {
|
||||
store.update {
|
||||
it.copy(registrationCheckpoint = checkpoint)
|
||||
}
|
||||
}
|
||||
|
||||
fun setPhoneNumber(phoneNumber: Phonenumber.PhoneNumber?) {
|
||||
store.update {
|
||||
it.copy(phoneNumber = phoneNumber)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchFcmToken(context: Context) {
|
||||
viewModelScope.launch {
|
||||
val fcmToken = RegistrationRepository.getFcmToken(context)
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED,
|
||||
isFcmSupported = true,
|
||||
fcmToken = fcmToken
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onUserConfirmedPhoneNumber(context: Context) {
|
||||
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
|
||||
// TODO [regv2]: check if can skip sms flow
|
||||
val state = store.value
|
||||
if (state.phoneNumber == null) {
|
||||
Log.w(TAG, "Phone number was null after confirmation.")
|
||||
onErrorOccurred()
|
||||
return
|
||||
}
|
||||
if (state.canSkipSms) {
|
||||
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
|
||||
} else {
|
||||
// TODO [regv2]: initialize Play Services sms retriever
|
||||
val mccMncProducer = MccMncProducer(context)
|
||||
val e164 = state.phoneNumber.toE164()
|
||||
viewModelScope.launch {
|
||||
val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow()
|
||||
store.update {
|
||||
it.copy(
|
||||
sessionId = codeRequestResponse.body.id,
|
||||
nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms),
|
||||
nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall),
|
||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyCodeWithoutRegistrationLock(context: Context, code: String) {
|
||||
store.update {
|
||||
it.copy(
|
||||
inProgress = true,
|
||||
registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED
|
||||
)
|
||||
}
|
||||
|
||||
val sessionId = store.value.sessionId
|
||||
if (sessionId == null) {
|
||||
Log.w(TAG, "Session ID was null. TODO: handle this better in the UI.")
|
||||
return
|
||||
}
|
||||
val e164: String = getCurrentE164() ?: throw IllegalStateException()
|
||||
|
||||
viewModelScope.launch {
|
||||
val registrationData = getRegistrationData(code)
|
||||
val verificationResponse = RegistrationRepository.submitVerificationCode(context, e164, password, sessionId, registrationData).successOrThrow()
|
||||
|
||||
if (!verificationResponse.body.verified) {
|
||||
Log.w(TAG, "Could not verify code!")
|
||||
// TODO [regv2]: error handling
|
||||
return@launch
|
||||
}
|
||||
|
||||
setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED)
|
||||
|
||||
val registrationResponse = RegistrationRepository.registerAccount(context, e164, password, sessionId, registrationData).successOrThrow()
|
||||
|
||||
localRegisterAccount(context, registrationData, registrationResponse, false)
|
||||
|
||||
refreshFeatureFlags()
|
||||
|
||||
store.update {
|
||||
it.copy(
|
||||
registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasPin(): Boolean {
|
||||
return RegistrationRepository.hasPin() || store.value.isReRegister
|
||||
}
|
||||
|
||||
fun completeRegistration() {
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(ProfileUploadJob())
|
||||
.then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob()))
|
||||
.enqueue()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
}
|
||||
|
||||
private fun getCurrentE164(): String? {
|
||||
return store.value.phoneNumber?.toE164()
|
||||
}
|
||||
|
||||
private suspend fun localRegisterAccount(
|
||||
context: Context,
|
||||
registrationData: RegistrationData,
|
||||
remoteResult: RegistrationRepository.AccountRegistrationResult,
|
||||
reglockEnabled: Boolean
|
||||
) {
|
||||
RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled)
|
||||
}
|
||||
|
||||
private suspend fun getRegistrationData(code: String): RegistrationData {
|
||||
val e164: String = getCurrentE164() ?: throw IllegalStateException()
|
||||
return RegistrationData(
|
||||
code,
|
||||
e164,
|
||||
password,
|
||||
RegistrationRepository.getRegistrationId(),
|
||||
RegistrationRepository.getProfileKey(e164),
|
||||
store.value.fcmToken,
|
||||
RegistrationRepository.getPniRegistrationId(),
|
||||
null // TODO [regv2]: recovery password
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a generic error UI handler that re-enables the UI so that the user can recover from errors.
|
||||
* Do not forget to log any errors when calling this method!
|
||||
*/
|
||||
private fun onErrorOccurred() {
|
||||
setInProgress(false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
|
||||
|
||||
private suspend fun refreshFeatureFlags() = withContext(Dispatchers.IO) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
try {
|
||||
FeatureFlags.refreshSync()
|
||||
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.")
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registration.v2.ui.welcome
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Binding
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
|
||||
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
|
||||
import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
/**
|
||||
* First screen that is displayed on the very first app launch.
|
||||
*/
|
||||
class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome_v2) {
|
||||
private val TAG = Log.tag(WelcomeV2Fragment::class.java)
|
||||
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
|
||||
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
maybePrefillE164()
|
||||
setDebugLogSubmitMultiTapView(binding.image)
|
||||
setDebugLogSubmitMultiTapView(binding.title)
|
||||
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
|
||||
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
|
||||
binding.welcomeTransferOrRestore.setOnClickListener { onRestoreFromBackupClicked() }
|
||||
}
|
||||
|
||||
private fun onContinueClicked() {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
|
||||
} else {
|
||||
skipRestore()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPermissions(): Boolean {
|
||||
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
|
||||
return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
private fun skipRestore() {
|
||||
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
|
||||
}
|
||||
|
||||
private fun onRestoreFromBackupClicked() {
|
||||
Toast.makeText(requireContext(), "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun onTermsClicked() {
|
||||
Toast.makeText(requireContext(), "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun maybePrefillE164() {
|
||||
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
|
||||
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
|
||||
|
||||
if (localNumber != null) {
|
||||
Log.v(TAG, "Phone number detected.")
|
||||
sharedViewModel.setPhoneNumber(localNumber)
|
||||
} else {
|
||||
Log.i(TAG, "Could not read phone number.")
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "No phone permission.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,9 +90,9 @@ public final class FeatureFlags {
|
|||
private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
|
||||
private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList";
|
||||
private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow";
|
||||
public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
|
||||
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
|
||||
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
|
||||
public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
|
||||
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
|
||||
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
|
||||
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
|
||||
private static final String PAYPAL_ONE_TIME_DONATIONS = "android.oneTimePayPalDonations.2";
|
||||
private static final String PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.3";
|
||||
|
@ -104,15 +104,15 @@ public final class FeatureFlags {
|
|||
private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch";
|
||||
private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode";
|
||||
private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch";
|
||||
public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
|
||||
public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
|
||||
private static final String PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig";
|
||||
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
|
||||
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback.1";
|
||||
public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
|
||||
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
|
||||
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback.1";
|
||||
public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
|
||||
private static final String SEPA_DEBIT_DONATIONS = "android.sepa.debit.donations.5";
|
||||
private static final String IDEAL_DONATIONS = "android.ideal.donations.5";
|
||||
public static final String IDEAL_ENABLED_REGIONS = "global.donations.idealEnabledRegions";
|
||||
public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
|
||||
public static final String IDEAL_ENABLED_REGIONS = "global.donations.idealEnabledRegions";
|
||||
public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
|
||||
private static final String CALLING_REACTIONS = "android.calling.reactions";
|
||||
private static final String NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist";
|
||||
private static final String CALLING_RAISE_HAND = "android.calling.raiseHand";
|
||||
|
@ -127,6 +127,7 @@ public final class FeatureFlags {
|
|||
private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds";
|
||||
private static final String MESSAGE_BACKUPS = "android.messageBackups";
|
||||
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
|
||||
private static final String REGISTRATION_V2 = "android.registration.v2";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -209,7 +210,7 @@ public final class FeatureFlags {
|
|||
);
|
||||
|
||||
@VisibleForTesting
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS);
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2);
|
||||
|
||||
/**
|
||||
* Values in this map will take precedence over any value. This should only be used for local
|
||||
|
@ -741,6 +742,11 @@ public final class FeatureFlags {
|
|||
return getBoolean(CAMERAX_CUSTOM_CONTROLLER, false);
|
||||
}
|
||||
|
||||
/** Whether or not to use the V2 refactor of registration. */
|
||||
public static boolean registrationV2() {
|
||||
return getBoolean(REGISTRATION_V2, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util.livedata
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
/**
|
||||
* A wrapper class that can be implemented in order to create a [LiveData] [Observer] that cleans up after itself.
|
||||
*
|
||||
* Useful for one-shot observers that can be executed as a callback on an asynchronous call that updates a [LiveData] upon completion.
|
||||
*/
|
||||
abstract class LiveDataObserverCallback<T>(private val liveData: LiveData<T>) : Observer<T> {
|
||||
final override fun onChanged(value: T) {
|
||||
val shouldRemove = onValue(value)
|
||||
if (shouldRemove) {
|
||||
liveData.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of the observer that gets executed when the value is changed.
|
||||
* Recommended usage is to check some condition in the [LiveData] to determine whether the data has been handled and therefore can be removed.
|
||||
*
|
||||
* @return should remove this observer from the [LiveData]
|
||||
*/
|
||||
abstract fun onValue(value: T): Boolean
|
||||
}
|
|
@ -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"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".registration.v2.ui.RegistrationV2Activity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
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_v2" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
141
app/src/main/res/layout/fragment_registration_enter_code_v2.xml
Normal file
141
app/src/main/res/layout/fragment_registration_enter_code_v2.xml
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?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.v2.ui.entercode.EnterCodeV2Fragment">
|
||||
|
||||
<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.EnterSmsCodeFragment">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/wrong_number"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="22dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="@dimen/registrationactivity_text_view_padding"
|
||||
android:hyphenationFrequency="normal"
|
||||
android:maxWidth="150dp"
|
||||
android:text="@string/RegistrationActivity_wrong_number"
|
||||
android:textColor="@color/signal_colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verification_subheader" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verify_header"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="40dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/RegistrationActivity_verification_code"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verification_subheader"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/verify_header"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verify_header"
|
||||
tools:text="@string/RegistrationActivity_enter_the_code_we_sent_to_s" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.ActionCountDownButton
|
||||
android:id="@+id/call_me_count_down"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/code"
|
||||
android:layout_marginEnd="22dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hyphenationFrequency="normal"
|
||||
android:maxWidth="150dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/code"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:text="@string/RegistrationActivity_call" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.ActionCountDownButton
|
||||
android:id="@+id/resend_sms_count_down"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/code"
|
||||
android:layout_marginStart="22dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hyphenationFrequency="normal"
|
||||
android:maxWidth="150dp"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/call_me_count_down"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/code"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:text="@string/RegistrationActivity_resend_code" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/having_trouble_button"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/RegistrationActivity_support_bottom_sheet_title"
|
||||
android:textColor="@color/signal_colorPrimary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/code" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.VerificationCodeView
|
||||
android:id="@+id/code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wrong_number" />
|
||||
|
||||
</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"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,133 @@
|
|||
<?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="org.thoughtcrime.securesms.registration.v2.ui.phonenumber.EnterPhoneNumberV2Fragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/verify_subheader">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/country_code"
|
||||
style="@style/Widget.Signal.TextInputLayout.Registration.Dropdown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:theme="@style/Signal.ThemeOverlay.TextInputLayout"
|
||||
app:errorEnabled="false"
|
||||
app:hintEnabled="false">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:digits="+1234567890"
|
||||
android:drawablePadding="-24dp"
|
||||
android:hint="@string/Registration_country_code_entry_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:maxLength="4"
|
||||
android:maxLines="1"
|
||||
android:padding="0dp"
|
||||
android:singleLine="true"
|
||||
tools:text="+1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/number"
|
||||
style="@style/Widget.Signal.TextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/RegistrationActivity_phone_number_description"
|
||||
app:materialThemeOverlay="@style/Signal.ThemeOverlay.TextInputLayout">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="phone">
|
||||
|
||||
<requestFocus />
|
||||
</com.google.android.material.textfield.TextInputEditText>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verify_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:text="@string/RegistrationActivity_phone_number"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/verify_subheader"
|
||||
style="@style/Signal.Text.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/RegistrationActivity_enter_your_phone_number_to_get_started"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verify_header"
|
||||
tools:layout_editor_absoluteX="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_button"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@android:string/cancel"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/registerButton"
|
||||
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:circularProgressMaterialButton__label="@string/RegistrationActivity_continue"
|
||||
app:layout_constraintBottom_toTopOf="@+id/cancel_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
71
app/src/main/res/layout/fragment_registration_welcome_v2.xml
Normal file
71
app/src/main/res/layout/fragment_registration_welcome_v2.xml
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?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">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/welcome"
|
||||
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/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message"
|
||||
android:textAppearance="@style/Signal.Text.HeadlineMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcome_terms_button"
|
||||
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
|
||||
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/welcome_terms_button"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/RegistrationActivity_terms_and_privacy"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcome_continue_button"
|
||||
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
|
||||
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/welcome_continue_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="17dp"
|
||||
app:circularProgressMaterialButton__label="@string/RegistrationActivity_continue"
|
||||
app:layout_constraintBottom_toTopOf="@id/welcome_transfer_or_restore"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin"
|
||||
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/welcome_transfer_or_restore"
|
||||
style="@style/Signal.Widget.Button.Large.Secondary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/registration_activity__transfer_or_restore_account"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
|
||||
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
492
app/src/main/res/navigation/registration_v2.xml
Normal file
492
app/src/main/res/navigation/registration_v2.xml
Normal file
|
@ -0,0 +1,492 @@
|
|||
<?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_v2"
|
||||
app:startDestination="@id/welcomeV2Fragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/welcomeV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.v2.ui.welcome.WelcomeV2Fragment"
|
||||
android:label="fragment_welcome"
|
||||
tools:layout="@layout/fragment_registration_welcome_v2">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restore"
|
||||
app:destination="@id/restoreBackupV2Fragment"
|
||||
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/enterPhoneNumberV2Fragment"
|
||||
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_transfer_or_restore"
|
||||
app:destination="@id/transferOrRestore"
|
||||
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_welcomeFragment_to_deviceTransferSetup"
|
||||
app:destination="@id/deviceTransferSetup"
|
||||
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_welcomeFragment_to_grantPermissionsV2Fragment"
|
||||
app:destination="@id/grantPermissionsV2Fragment"
|
||||
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/grantPermissionsV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment"
|
||||
android:label="fragment_grant_permissions">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restore"
|
||||
app:destination="@id/restoreBackupV2Fragment"
|
||||
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/enterPhoneNumberV2Fragment"
|
||||
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_transfer_or_restore"
|
||||
app:destination="@id/transferOrRestore"
|
||||
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" />
|
||||
|
||||
<argument
|
||||
android:name="welcomeAction"
|
||||
app:argType="org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment$WelcomeAction" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/chooseBackupV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.ChooseBackupFragment"
|
||||
android:label="fragment_choose_backup"
|
||||
tools:layout="@layout/fragment_registration_choose_backup">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restore"
|
||||
app:destination="@id/restoreBackupV2Fragment"
|
||||
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/chooseBackupV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_skip"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
app:enterAnim="@anim/slide_from_end"
|
||||
app:exitAnim="@anim/slide_to_start"
|
||||
app:popEnterAnim="@anim/slide_from_start"
|
||||
app:popExitAnim="@anim/slide_to_end" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/enterPhoneNumberV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.v2.ui.phonenumber.EnterPhoneNumberV2Fragment"
|
||||
android:label="fragment_enter_phone_number"
|
||||
tools:layout="@layout/fragment_registration_enter_phone_number_v2">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_pickCountry"
|
||||
app:destination="@id/countryPickerV2Fragment"
|
||||
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/enterCodeV2Fragment"
|
||||
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/captchaV2Fragment"
|
||||
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_editProxy"
|
||||
app:destination="@+id/registrationProxyV2Fragment"
|
||||
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_reRegisterWithPinV2Fragment"
|
||||
app:destination="@id/reRegisterWithPinV2Fragment"
|
||||
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/enterPhoneNumberV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/countryPickerV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
|
||||
android:label="fragment_country_picker"
|
||||
tools:layout="@layout/fragment_registration_country_picker" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/enterCodeV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.v2.ui.entercode.EnterCodeV2Fragment"
|
||||
android:label="fragment_enter_code"
|
||||
tools:layout="@layout/fragment_registration_enter_code_v2">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_requireKbsLockPin"
|
||||
app:destination="@id/registrationLockV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_requestCaptcha"
|
||||
app:destination="@id/captchaV2Fragment"
|
||||
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/registrationCompletePlaceHolderV2Fragment"
|
||||
app:popUpTo="@+id/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_accountLocked"
|
||||
app:destination="@id/accountLockedV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/registrationLockV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationLockFragment"
|
||||
android:label="fragment_kbs_lock"
|
||||
tools:layout="@layout/fragment_registration_lock">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_successfulRegistration"
|
||||
app:destination="@id/registrationCompletePlaceHolderV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_accountLocked"
|
||||
app:destination="@id/accountLockedV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/reRegisterWithPinV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
|
||||
tools:layout="@layout/fragment_registration_lock">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderV2Fragment"
|
||||
app:destination="@id/registrationCompletePlaceHolderV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_reRegisterWithPinFragment_to_enterPhoneNumberV2Fragment"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
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/reRegisterWithPinV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/accountLockedV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
|
||||
android:label="fragment_account_locked"
|
||||
tools:layout="@layout/account_locked_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/captchaV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
|
||||
android:label="fragment_captcha"
|
||||
tools:layout="@layout/fragment_registration_captcha" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/restoreBackupV2Fragment"
|
||||
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/enterPhoneNumberV2Fragment"
|
||||
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/restoreBackupV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_skip"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
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/enterPhoneNumberV2Fragment"
|
||||
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/restoreBackupV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_skip_no_return"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
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/restoreBackupV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<argument
|
||||
android:name="uri"
|
||||
android:defaultValue="@null"
|
||||
app:argType="android.net.Uri"
|
||||
app:nullable="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/registrationCompletePlaceHolderV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationCompleteFragment"
|
||||
android:label="fragment_registration_complete_place_holder"
|
||||
tools:layout="@layout/fragment_registration_blank" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/registrationProxyV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.preferences.EditProxyFragment"
|
||||
android:label="fragment_registration_edit_proxy"
|
||||
tools:layout="@layout/edit_proxy_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/transferOrRestore"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.TransferOrRestoreFragment"
|
||||
tools:layout="@layout/fragment_transfer_restore">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_choose_backup"
|
||||
app:destination="@id/chooseBackupV2Fragment"
|
||||
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_new_device_transfer_instructions"
|
||||
app:destination="@id/newDeviceTransferInstructions"
|
||||
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_transferOrRestore_to_moreOptions"
|
||||
app:destination="@+id/transferOrRestoreMoreOptionsDialog" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/restoreFromBackupV2Fragment"
|
||||
android:name="org.thoughtcrime.securesms.backup.v2.ui.restore.RestoreFromBackupFragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restoreFromBacakupFragment_to_moreOptions"
|
||||
app:destination="@+id/transferOrRestoreMoreOptionsDialog" />
|
||||
|
||||
<argument
|
||||
android:name="cancelable"
|
||||
app:argType="boolean" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/transferOrRestoreMoreOptionsDialog"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsSheet">
|
||||
|
||||
<argument
|
||||
android:name="mode"
|
||||
app:argType="org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode" />
|
||||
</dialog>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/newDeviceTransferInstructions"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferInstructionsFragment"
|
||||
tools:layout="@layout/new_device_transfer_instructions_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_device_transfer_setup"
|
||||
app:destination="@id/deviceTransferSetup"
|
||||
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/deviceTransferSetup"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferSetupFragment"
|
||||
tools:layout="@layout/device_transfer_setup_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_new_device_transfer"
|
||||
app:destination="@id/newDeviceTransfer"
|
||||
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_deviceTransferSetup_to_transferOrRestore"
|
||||
app:destination="@id/transferOrRestore"
|
||||
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/transferOrRestore"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/newDeviceTransfer"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferFragment"
|
||||
tools:layout="@layout/device_transfer_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_newDeviceTransfer_to_newDeviceTransferInstructions"
|
||||
app:destination="@id/newDeviceTransferInstructions"
|
||||
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/newDeviceTransferInstructions"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_newDeviceTransfer_to_newDeviceTransferComplete"
|
||||
app:destination="@id/newDeviceTransferComplete"
|
||||
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/welcomeV2Fragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/newDeviceTransferComplete"
|
||||
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferCompleteFragment"
|
||||
tools:layout="@layout/new_device_transfer_complete_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_newDeviceTransferComplete_to_enterPhoneNumberV2Fragment"
|
||||
app:destination="@id/enterPhoneNumberV2Fragment"
|
||||
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>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_restart_to_welcomeV2Fragment"
|
||||
app:destination="@id/welcomeV2Fragment"
|
||||
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/welcomeV2Fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</navigation>
|
|
@ -13,7 +13,6 @@ import org.signal.libsignal.protocol.IdentityKeyPair;
|
|||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.signal.libsignal.usernames.Username.UsernameLink;
|
||||
|
@ -47,6 +46,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
|
@ -219,7 +219,7 @@ public class SignalServiceAccountManager {
|
|||
|
||||
public ServiceResponse<RegistrationSessionMetadataResponse> createRegistrationSession(@Nullable String fcmToken, @Nullable String mcc, @Nullable String mnc) {
|
||||
try {
|
||||
final RegistrationSessionMetadataResponse response = pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc);
|
||||
final RegistrationSessionMetadataResponse response = pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc);
|
||||
return ServiceResponse.forResult(response, 200, null);
|
||||
} catch (IOException e) {
|
||||
return ServiceResponse.forUnknownError(e);
|
||||
|
@ -311,6 +311,10 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public @Nonnull VerifyAccountResponse registerAccountV2(@Nullable String sessionId, @Nullable String recoveryPassword, AccountAttributes attributes, PreKeyCollection aciPreKeys, PreKeyCollection pniPreKeys, String fcmToken, boolean skipDeviceTransfer) throws IOException {
|
||||
return pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer);
|
||||
}
|
||||
|
||||
public @Nonnull ServiceResponse<VerifyAccountResponse> changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) {
|
||||
try {
|
||||
VerifyAccountResponse response = this.pushServiceSocket.changeNumber(changePhoneNumberRequest);
|
||||
|
@ -870,6 +874,10 @@ public class SignalServiceAccountManager {
|
|||
return KeysApi.create(pushServiceSocket);
|
||||
}
|
||||
|
||||
public RegistrationApi getRegistrationApi() {
|
||||
return new RegistrationApi(pushServiceSocket);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return pushServiceSocket.getPaymentsAuthorization();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.registration
|
||||
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Class to interact with various registration-related endpoints.
|
||||
*/
|
||||
class RegistrationApi(
|
||||
private val pushServiceSocket: PushServiceSocket
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request that the service initialize a new registration session.
|
||||
*/
|
||||
fun createRegistrationSession(fcmToken: String?, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit an FCM token to the service as proof that this is an honest user attempting to register.
|
||||
*/
|
||||
fun submitPushChallengeToken(sessionId: String?, pushChallengeToken: String?): NetworkResult<RegistrationSessionMetadataResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.patchVerificationSession(sessionId, null, null, null, null, pushChallengeToken)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an SMS verification code. On success, the server will send
|
||||
* an SMS verification code to this Signal user.
|
||||
*
|
||||
* @param androidSmsRetrieverSupported whether the system framework will automatically parse the incoming verification message.
|
||||
*/
|
||||
fun requestSmsVerificationCode(sessionId: String?, locale: Locale?, androidSmsRetrieverSupported: Boolean): NetworkResult<RegistrationSessionMetadataResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.requestVerificationCode(sessionId, locale, androidSmsRetrieverSupported, PushServiceSocket.VerificationCodeTransport.SMS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a verification code sent by the service via one of the supported channels (SMS, phone call) to prove the registrant's control of the phone number.
|
||||
*/
|
||||
fun verifyAccount(verificationCode: String, sessionId: String): NetworkResult<RegistrationSessionMetadataResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.submitVerificationCode(sessionId, verificationCode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the cryptographic assets required for an account to use the service.
|
||||
*/
|
||||
fun registerAccount(sessionId: String?, recoveryPassword: String?, attributes: AccountAttributes?, aciPreKeys: PreKeyCollection?, pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean): NetworkResult<VerifyAccountResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
@SuppressWarnings("WeakerAccess")
|
||||
public final class MediaConverter {
|
||||
private static final String TAG = "media-converter";
|
||||
private static final boolean VERBOSE = false; // lots of logging
|
||||
private static final boolean VERBOSE = true; // lots of logging
|
||||
|
||||
// Describes when the annotation will be discarded
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
Loading…
Add table
Reference in a new issue