From eec2685e67ec5d5c3e6ac79f43aa8053834bdbc2 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Fri, 12 Apr 2024 12:47:27 -0400 Subject: [PATCH] Registration refactor initial scaffolding. --- app/src/main/AndroidManifest.xml | 7 + .../securesms/PassphraseRequiredActivity.java | 8 +- .../registration/VerificationCodeView.kt | 2 +- .../registration/PushChallengeRequest.java | 2 +- .../registration/RegistrationData.kt | 1 + .../compose/GrantPermissionsScreen.kt | 212 ++++++++ .../fragments/EnterPhoneNumberFragment.java | 9 +- .../fragments/GrantPermissionsFragment.kt | 202 +------ .../SignalStrengthPhoneStateListener.java | 13 +- .../v2/data/RegistrationRepository.kt | 392 ++++++++++++++ .../v2/ui/RegistrationV2Activity.kt | 40 ++ .../v2/ui/RegistrationV2Extensions.kt | 13 + .../v2/ui/entercode/EnterCodeV2Fragment.kt | 128 +++++ .../GrantPermissionsV2Fragment.kt | 113 ++++ .../phonenumber/EnterPhoneNumberV2Fragment.kt | 338 ++++++++++++ .../ui/phonenumber/EnterPhoneNumberV2State.kt | 25 + .../EnterPhoneNumberV2ViewModel.kt | 93 ++++ .../v2/ui/shared/RegistrationCheckpoint.kt | 27 + .../v2/ui/shared/RegistrationV2State.kt | 24 + .../v2/ui/shared/RegistrationV2ViewModel.kt | 221 ++++++++ .../v2/ui/welcome/WelcomeV2Fragment.kt | 90 ++++ .../securesms/util/FeatureFlags.java | 26 +- .../util/livedata/LiveDataObserverCallback.kt | 31 ++ .../activity_registration_navigation_v2.xml | 22 + .../fragment_registration_enter_code_v2.xml | 141 +++++ ...ent_registration_enter_phone_number_v2.xml | 133 +++++ .../fragment_registration_welcome_v2.xml | 71 +++ .../main/res/navigation/registration_v2.xml | 492 ++++++++++++++++++ .../api/SignalServiceAccountManager.java | 12 +- .../api/registration/RegistrationApi.kt | 70 +++ .../video/videoconverter/MediaConverter.java | 2 +- 31 files changed, 2732 insertions(+), 228 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/compose/GrantPermissionsScreen.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationCheckpoint.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2State.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2ViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataObserverCallback.kt create mode 100644 app/src/main/res/layout/activity_registration_navigation_v2.xml create mode 100644 app/src/main/res/layout/fragment_registration_enter_code_v2.xml create mode 100644 app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml create mode 100644 app/src/main/res/layout/fragment_registration_welcome_v2.xml create mode 100644 app/src/main/res/navigation/registration_v2.xml create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b292a8337c..56b494aa38 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -837,6 +837,13 @@ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:exported="false"/> + + 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) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java index 0855f90e5c..aa5c9a0864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt index 94ccdbdab8..c37801b045 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/GrantPermissionsFragment.kt @@ -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) - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java index fded5f0aaf..a751313473 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/SignalStrengthPhoneStateListener.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt new file mode 100644 index 0000000000..a1f583103e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -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 = + 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 = + 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 = + 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 = + 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(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 + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt new file mode 100644 index 0000000000..1271a2133b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt new file mode 100644 index 0000000000..9b76f73561 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Extensions.kt @@ -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) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt new file mode 100644 index 0000000000..225504ae13 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt @@ -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() + 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 + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt new file mode 100644 index 0000000000..e61987787c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt @@ -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() + private val args by navArgs() + 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) { + 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt new file mode 100644 index 0000000000..cca66bb7e7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt @@ -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() + private val fragmentViewModel by viewModels() + private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind) + + private lateinit var spinnerAdapter: ArrayAdapter + 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( + 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(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 + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt new file mode 100644 index 0000000000..6f399443f6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2State.kt @@ -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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt new file mode 100644 index 0000000000..4a7804f67e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2ViewModel.kt @@ -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 = 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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationCheckpoint.kt new file mode 100644 index 0000000000..744268df98 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationCheckpoint.kt @@ -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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2State.kt new file mode 100644 index 0000000000..45511acf0b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2State.kt @@ -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 +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2ViewModel.kt new file mode 100644 index 0000000000..e20449fe92 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/shared/RegistrationV2ViewModel.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt new file mode 100644 index 0000000000..1abb0512ba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt @@ -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() + 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.") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 08f450dff4..b5baf4fb4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -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 NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS); + static final Set 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 getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataObserverCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataObserverCallback.kt new file mode 100644 index 0000000000..8f6caff24b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataObserverCallback.kt @@ -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(private val liveData: LiveData) : Observer { + 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 +} diff --git a/app/src/main/res/layout/activity_registration_navigation_v2.xml b/app/src/main/res/layout/activity_registration_navigation_v2.xml new file mode 100644 index 0000000000..37a71416cd --- /dev/null +++ b/app/src/main/res/layout/activity_registration_navigation_v2.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_enter_code_v2.xml b/app/src/main/res/layout/fragment_registration_enter_code_v2.xml new file mode 100644 index 0000000000..3d30f729dd --- /dev/null +++ b/app/src/main/res/layout/fragment_registration_enter_code_v2.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml b/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml new file mode 100644 index 0000000000..fea95edc37 --- /dev/null +++ b/app/src/main/res/layout/fragment_registration_enter_phone_number_v2.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_welcome_v2.xml b/app/src/main/res/layout/fragment_registration_welcome_v2.xml new file mode 100644 index 0000000000..5f5a350be4 --- /dev/null +++ b/app/src/main/res/layout/fragment_registration_welcome_v2.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/registration_v2.xml b/app/src/main/res/navigation/registration_v2.xml new file mode 100644 index 0000000000..9d363338d1 --- /dev/null +++ b/app/src/main/res/navigation/registration_v2.xml @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 9e2e8ef7f1..801615e3d5 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -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 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 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(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt new file mode 100644 index 0000000000..80dce4bc2e --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + return NetworkResult.fromFetch { + pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer) + } + } +} diff --git a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java index 7d780d50ea..0b41c96c40 100644 --- a/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java +++ b/video/lib/src/main/java/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java @@ -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)