Registration refactor initial scaffolding.

This commit is contained in:
Nicholas Tinsley 2024-04-12 12:47:27 -04:00 committed by Greyson Parrelli
parent 318b59a6b2
commit eec2685e67
31 changed files with 2732 additions and 228 deletions

View file

@ -837,6 +837,13 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.v2.ui.RegistrationV2Activity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"

View file

@ -28,8 +28,10 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@ -208,7 +210,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
if (FeatureFlags.registrationV2()) {
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
} else {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
}
}
private Intent getEnterSignalPinIntent() {

View file

@ -59,7 +59,7 @@ class VerificationCodeView @JvmOverloads constructor(context: Context, attrs: At
}
}
interface OnCodeEnteredListener {
fun interface OnCodeEnteredListener {
fun onCodeComplete(code: String)
}

View file

@ -110,7 +110,7 @@ public final class PushChallengeRequest {
}
}
static class PushChallengeEvent {
public static class PushChallengeEvent {
private final String challenge;
PushChallengeEvent(String challenge) {

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.registration
import org.signal.libsignal.zkgroup.profiles.ProfileKey
// TODO [regv2]: fold sessionId into this?
data class RegistrationData(
val code: String,
val e164: String,

View file

@ -0,0 +1,212 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
@Preview
@Composable
fun GrantPermissionsScreenPreview() {
SignalTheme(isDarkMode = false) {
GrantPermissionsScreen(
deviceBuildVersion = 33,
isBackupSelectionRequired = true,
isSearchingForBackup = true,
{},
{}
)
}
}
/**
* Layout that explains permissions rationale to the user.
*/
@Composable
fun GrantPermissionsScreen(
deviceBuildVersion: Int,
isBackupSelectionRequired: Boolean,
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit,
onNotNowClicked: () -> Unit
) {
Surface {
Column(
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 40.dp, bottom = 24.dp)
) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__allow_permissions),
style = MaterialTheme.typography.headlineMedium
)
}
item {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__to_help_you_message_people_you_know),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 12.dp, bottom = 41.dp)
)
}
if (deviceBuildVersion >= 33) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_contact),
title = stringResource(id = R.string.GrantPermissionsFragment__contacts),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__find_people_you_know)
)
}
if (deviceBuildVersion < 29 || !isBackupSelectionRequired) {
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_file),
title = stringResource(id = R.string.GrantPermissionsFragment__storage),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__send_photos_videos_and_files)
)
}
}
item {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_phone),
title = stringResource(id = R.string.GrantPermissionsFragment__phone_calls),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__make_registering_easier)
)
}
}
Row {
TextButton(onClick = onNotNowClicked) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__not_now)
)
}
Spacer(modifier = Modifier.weight(1f))
if (isSearchingForBackup) {
Box {
NextButton(
isSearchingForBackup = true,
onNextClicked = onNextClicked
)
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
} else {
NextButton(
isSearchingForBackup = false,
onNextClicked = onNextClicked
)
}
}
}
}
}
@Preview
@Composable
fun PermissionRowPreview() {
PermissionRow(
imageVector = ImageVector.vectorResource(id = R.drawable.permission_notification),
title = stringResource(id = R.string.GrantPermissionsFragment__notifications),
subtitle = stringResource(id = R.string.GrantPermissionsFragment__get_notified_when)
)
}
@Composable
fun PermissionRow(
imageVector: ImageVector,
title: String,
subtitle: String
) {
Row(modifier = Modifier.padding(bottom = 32.dp)) {
Image(
imageVector = imageVector,
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall
)
Text(
text = subtitle,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.size(32.dp))
}
}
@Composable
fun NextButton(
isSearchingForBackup: Boolean,
onNextClicked: () -> Unit
) {
val alpha = if (isSearchingForBackup) {
0f
} else {
1f
}
Buttons.LargeTonal(
onClick = onNextClicked,
enabled = !isSearchingForBackup,
modifier = Modifier.alpha(alpha)
) {
Text(
text = stringResource(id = R.string.GrantPermissionsFragment__next)
)
}
}

View file

@ -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);

View file

@ -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)
)
}
}

View file

@ -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();

View file

@ -0,0 +1,392 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.data
import android.content.Context
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.AppCapabilities
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.gcm.FcmUtil
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.pin.SvrRepository.onRegistrationComplete
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.PushChallengeRequest
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.service.DirectoryRefreshListener
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds
/**
* A repository that deals with disk I/O during account registration.
*/
object RegistrationRepository {
private val TAG = Log.tag(RegistrationRepository::class.java)
private val PUSH_REQUEST_TIMEOUT = 5.seconds.inWholeMilliseconds
/**
* Retrieve the FCM token from the Firebase service.
*/
suspend fun getFcmToken(context: Context): String? =
withContext(Dispatchers.Default) {
FcmUtil.getToken(context).orElse(null)
}
/**
* Queries the local store for whether a PIN is set.
*/
@JvmStatic
fun hasPin(): Boolean {
return SignalStore.svr().hasPin()
}
/**
* Queries, and creates if needed, the local registration ID.
*/
@JvmStatic
fun getRegistrationId(): Int {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
var registrationId = SignalStore.account().registrationId
if (registrationId == 0) {
registrationId = KeyHelper.generateRegistrationId(false)
SignalStore.account().registrationId = registrationId
}
return registrationId
}
/**
* Queries, and creates if needed, the local PNI registration ID.
*/
@JvmStatic
fun getPniRegistrationId(): Int {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
var pniRegistrationId = SignalStore.account().pniRegistrationId
if (pniRegistrationId == 0) {
pniRegistrationId = KeyHelper.generateRegistrationId(false)
SignalStore.account().pniRegistrationId = pniRegistrationId
}
return pniRegistrationId
}
/**
* Queries, and creates if needed, the local profile key.
*/
@JvmStatic
suspend fun getProfileKey(e164: String): ProfileKey =
withContext(Dispatchers.IO) {
// TODO [regv2]: make creation more explicit instead of hiding it in this getter
val recipientTable = SignalDatabase.recipients
val recipient = recipientTable.getByE164(e164)
var profileKey = if (recipient.isPresent) {
ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).profileKey)
} else {
null
}
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew()
Log.i(TAG, "No profile key found, created a new one")
}
profileKey
}
/**
* Takes a server response from a successful registration and persists the relevant data.
*/
@WorkerThread
@JvmStatic
suspend fun registerAccountLocally(context: Context, registrationData: RegistrationData, response: AccountRegistrationResult, reglockEnabled: Boolean) =
withContext(Dispatchers.IO) {
val aciPreKeyCollection: PreKeyCollection = response.aciPreKeyCollection
val pniPreKeyCollection: PreKeyCollection = response.pniPreKeyCollection
val aci: ACI = ACI.parseOrThrow(response.uuid)
val pni: PNI = PNI.parseOrThrow(response.pni)
val hasPin: Boolean = response.storageCapable
SignalStore.account().setAci(aci)
SignalStore.account().setPni(pni)
ApplicationDependencies.resetProtocolStores()
ApplicationDependencies.getProtocolStore().aci().sessions().archiveAllSessions()
ApplicationDependencies.getProtocolStore().pni().sessions().archiveAllSessions()
SenderKeyUtil.clearAllState()
val aciProtocolStore = ApplicationDependencies.getProtocolStore().aci()
val aciMetadataStore = SignalStore.account().aciPreKeys
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
storeSignedAndLastResortPreKeys(aciProtocolStore, aciMetadataStore, aciPreKeyCollection)
storeSignedAndLastResortPreKeys(pniProtocolStore, pniMetadataStore, pniPreKeyCollection)
val recipientTable = SignalDatabase.recipients
val selfId = Recipient.trustedPush(aci, pni, registrationData.e164).id
recipientTable.setProfileSharing(selfId, true)
recipientTable.markRegisteredOrThrow(selfId, aci)
recipientTable.linkIdsForSelf(aci, pni, registrationData.e164)
recipientTable.setProfileKey(selfId, registrationData.profileKey)
ApplicationDependencies.getRecipientCache().clearSelf()
SignalStore.account().setE164(registrationData.e164)
SignalStore.account().fcmToken = registrationData.fcmToken
SignalStore.account().fcmEnabled = registrationData.isFcm
val now = System.currentTimeMillis()
saveOwnIdentityKey(selfId, aci, aciProtocolStore, now)
saveOwnIdentityKey(selfId, pni, pniProtocolStore, now)
SignalStore.account().setServicePassword(registrationData.password)
SignalStore.account().setRegistered(true)
TextSecurePreferences.setPromptedPushRegistration(context, true)
TextSecurePreferences.setUnauthorizedReceived(context, false)
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
ApplicationDependencies.closeConnections()
ApplicationDependencies.getIncomingMessageObserver()
PreKeysSyncJob.enqueue()
val jobManager = ApplicationDependencies.getJobManager()
jobManager.add(DirectoryRefreshJob(false))
jobManager.add(RotateCertificateJob())
DirectoryRefreshListener.schedule(context)
RotateSignedPreKeyListener.schedule(context)
}
@JvmStatic
private fun saveOwnIdentityKey(selfId: RecipientId, serviceId: ServiceId, protocolStore: SignalServiceAccountDataStoreImpl, now: Long) {
protocolStore.identities().saveIdentityWithoutSideEffects(
selfId,
serviceId,
protocolStore.identityKeyPair.publicKey,
IdentityTable.VerifiedStatus.VERIFIED,
true,
now,
true
)
}
@JvmStatic
private fun storeSignedAndLastResortPreKeys(protocolStore: SignalServiceAccountDataStoreImpl, metadataStore: PreKeyMetadataStore, preKeyCollection: PreKeyCollection) {
PreKeyUtil.storeSignedPreKey(protocolStore, metadataStore, preKeyCollection.signedPreKey)
metadataStore.isSignedPreKeyRegistered = true
metadataStore.activeSignedPreKeyId = preKeyCollection.signedPreKey.id
metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis()
PreKeyUtil.storeLastResortKyberPreKey(protocolStore, metadataStore, preKeyCollection.lastResortKyberPreKey)
metadataStore.lastResortKyberPreKeyId = preKeyCollection.lastResortKyberPreKey.id
metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis()
}
/**
* Asks the service to send a verification code through one of our supported channels (SMS, phone call).
* This requires two or more network calls:
* 1. Create (or reuse) a session.
* 2. (Optional) If the session has any proof requirements ("challenges"), the user must solve them and submit the proof.
* 3. Once the service responds we are allowed to, we request the verification code.
*/
suspend fun requestSmsCode(context: Context, e164: String, password: String, mcc: String?, mnc: String?, mode: Mode = Mode.SMS_WITHOUT_LISTENER): NetworkResult<RegistrationSessionMetadataResponse> =
withContext(Dispatchers.IO) {
val fcmToken: String? = FcmUtil.getToken(context).orElse(null)
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val activeSession = if (fcmToken == null) {
// TODO [regv2]
val notImplementedError = NotImplementedError()
Log.w(TAG, "Not yet implemented!", notImplementedError)
NetworkResult.ApplicationError(notImplementedError)
} else {
createSessionAndBlockForPushChallenge(api, fcmToken, mcc, mnc)
}
activeSession.then { session ->
val sessionId = session.body.id
SignalStore.registrationValues().sessionId = sessionId
SignalStore.registrationValues().sessionE164 = e164
if (!session.body.allowedToRequestCode) {
val challenges = session.body.requestedInformation.joinToString()
Log.w(TAG, "Not allowed to request code! Remaining challenges: $challenges")
// TODO [regv2]: actually handle challenges
}
// TODO [regv2]: support other verification code [Mode] options
if (mode == Mode.PHONE_CALL) {
// TODO [regv2]
val notImplementedError = NotImplementedError()
Log.w(TAG, "Not yet implemented!", notImplementedError)
NetworkResult.ApplicationError(notImplementedError)
} else {
api.requestSmsVerificationCode(sessionId, Locale.getDefault(), mode.isSmsRetrieverSupported)
}
}
}
/**
* Submits the user-entered verification code to the service.
*/
suspend fun submitVerificationCode(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData): NetworkResult<RegistrationSessionMetadataResponse> =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
api.verifyAccount(registrationData.code, sessionId)
}
/**
* Submit the necessary assets as a verified account so that the user can actually use the service.
*/
suspend fun registerAccount(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult<AccountRegistrationResult> =
withContext(Dispatchers.IO) {
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
val masterKey: MasterKey? = masterKeyProducer?.produceMasterKey()
val registrationLock: String? = masterKey?.deriveRegistrationLock()
val accountAttributes = AccountAttributes(
signalingKey = null,
registrationId = registrationData.registrationId,
fetchesMessages = registrationData.isNotFcm,
registrationLock = registrationLock,
unidentifiedAccessKey = unidentifiedAccessKey,
unrestrictedUnidentifiedAccess = universalUnidentifiedAccess,
capabilities = AppCapabilities.getCapabilities(true),
discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE,
name = null,
pniRegistrationId = registrationData.pniRegistrationId,
recoveryPassword = registrationData.recoveryPassword
)
SignalStore.account().generateAciIdentityKeyIfNecessary()
val aciIdentity: IdentityKeyPair = SignalStore.account().aciIdentityKey
SignalStore.account().generatePniIdentityKeyIfNecessary()
val pniIdentity: IdentityKeyPair = SignalStore.account().pniIdentityKey
val aciPreKeyCollection = org.thoughtcrime.securesms.registration.RegistrationRepository.generateSignedAndLastResortPreKeys(aciIdentity, SignalStore.account().aciPreKeys)
val pniPreKeyCollection = org.thoughtcrime.securesms.registration.RegistrationRepository.generateSignedAndLastResortPreKeys(pniIdentity, SignalStore.account().pniPreKeys)
api.registerAccount(sessionId, registrationData.recoveryPassword, accountAttributes, aciPreKeyCollection, pniPreKeyCollection, registrationData.fcmToken, true)
.map { accountRegistrationResponse ->
AccountRegistrationResult(
uuid = accountRegistrationResponse.uuid,
pni = accountRegistrationResponse.pni,
storageCapable = accountRegistrationResponse.storageCapable,
number = accountRegistrationResponse.number,
masterKey = masterKey,
pin = pin,
aciPreKeyCollection = aciPreKeyCollection,
pniPreKeyCollection = pniPreKeyCollection
)
}
}
private suspend fun createSessionAndBlockForPushChallenge(accountManager: RegistrationApi, fcmToken: String, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> =
withContext(Dispatchers.IO) {
// TODO [regv2]: do not use event bus nor latch
val subscriber = PushTokenChallengeSubscriber()
val eventBus = EventBus.getDefault()
eventBus.register(subscriber)
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc).successOrThrow() // TODO: error handling
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
eventBus.unregister(subscriber)
if (receivedPush) {
val challenge = subscriber.challenge
if (challenge != null) {
Log.w(TAG, "Push challenge token received.")
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.body.id, challenge)
} else {
Log.w(TAG, "Push received but challenge token was null.")
}
} else {
Log.i(TAG, "Push challenge timed out.")
}
Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.")
return@withContext NetworkResult.ApplicationError<RegistrationSessionMetadataResponse>(NullPointerException())
}
@JvmStatic
fun deriveTimestamp(headers: RegistrationSessionMetadataHeaders, deltaSeconds: Int?): Long {
if (deltaSeconds == null) {
return 0L
}
val timestamp: Long = headers.timestamp
return timestamp + deltaSeconds.seconds.inWholeMilliseconds
}
enum class Mode(val isSmsRetrieverSupported: Boolean) {
SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false)
}
private class PushTokenChallengeSubscriber {
var challenge: String? = null
val latch = CountDownLatch(1)
@Subscribe
fun onChallengeEvent(pushChallengeEvent: PushChallengeRequest.PushChallengeEvent) {
challenge = pushChallengeEvent.challenge
latch.countDown()
}
}
data class AccountRegistrationResult(
val uuid: String,
val pni: String,
val storageCapable: Boolean,
val number: String,
val masterKey: MasterKey?,
val pin: String?,
val aciPreKeyCollection: PreKeyCollection,
val pniPreKeyCollection: PreKeyCollection
)
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}

View file

@ -0,0 +1,128 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.entercode
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.navigation.ActivityNavigator
import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
/**
* The final screen of account registration, where the user enters their verification code.
*/
class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_code_v2) {
private val TAG = Log.tag(EnterCodeV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val binding: FragmentRegistrationEnterCodeV2Binding by ViewBinderDelegate(FragmentRegistrationEnterCodeV2Binding::bind)
private lateinit var phoneStateListener: SignalStrengthPhoneStateListener
private var autopilotCodeEntryActive = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(binding.verifyHeader)
phoneStateListener = SignalStrengthPhoneStateListener(this, PhoneStateCallback())
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
popBackStack()
}
}
)
binding.wrongNumber.setOnClickListener {
popBackStack()
}
binding.code.setOnCompleteListener {
sharedViewModel.verifyCodeWithoutRegistrationLock(requireContext(), it)
}
binding.keyboard.setOnKeyPressListener { key ->
if (!autopilotCodeEntryActive) {
if (key >= 0) {
binding.code.append(key)
} else {
binding.code.delete()
}
}
}
sharedViewModel.uiState.observe(viewLifecycleOwner) {
if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) {
handleSuccessfulVerify()
}
}
}
private fun handleSuccessfulVerify() {
// TODO [regv2]: add functionality of [RegistrationCompleteFragment]
val activity = requireActivity()
val isProfileNameEmpty = Recipient.self().profileName.isEmpty
val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id)
val needsProfile = isProfileNameEmpty || isAvatarEmpty
val needsPin = !sharedViewModel.hasPin()
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
val startIntent = MainActivity.clearTop(activity).apply {
if (needsPin) {
putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(activity))
}
if (needsProfile) {
putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(activity))
}
}
activity.startActivity(startIntent)
sharedViewModel.setInProgress(false)
activity.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
}
private fun popBackStack() {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED)
NavHostFragment.findNavController(this).popBackStack()
}
private class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
override fun onNoCellSignalPresent() {
// TODO [regv2]: animate in bottom sheet
}
override fun onCellSignalPresent() {
// TODO [regv2]: animate in bottom sheet
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Screen in account registration that provides rationales for the suggested runtime permissions.
*/
@RequiresApi(23)
class GrantPermissionsV2Fragment : ComposeFragment() {
private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val args by navArgs<GrantPermissionsV2FragmentArgs>()
private val isSearchingForBackup = mutableStateOf(false)
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
::permissionsGranted
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.uiState.observe(viewLifecycleOwner) {
if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) {
proceedToNextScreen(it)
}
}
}
private fun proceedToNextScreen(it: RegistrationV2State) {
// TODO [nicholas]: conditionally go to backup flow
NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore())
}
@Composable
override fun FragmentContent() {
val isSearchingForBackup by this.isSearchingForBackup
GrantPermissionsScreen(
deviceBuildVersion = Build.VERSION.SDK_INT,
isSearchingForBackup = isSearchingForBackup,
isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current),
onNextClicked = this::onNextClicked,
onNotNowClicked = this::onNotNowClicked
)
}
private fun onNextClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> continueNext()
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
}
private fun continueNext() {
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired)
requestPermissionLauncher.launch(requiredPermissions)
}
private fun onNotNowClicked() {
when (args.welcomeAction) {
WelcomeAction.CONTINUE -> continueNotNow()
WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
}
private fun continueNotNow() {
NavHostFragment.findNavController(this).popBackStack()
}
private fun permissionsGranted(permissions: Map<String, Boolean>) {
permissions.forEach {
Log.d(TAG, "${it.key} = ${it.value}")
}
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
}
/**
* Which welcome action the user selected which prompted this
* screen.
*/
enum class WelcomeAction {
CONTINUE,
RESTORE_BACKUP
}
}

View file

@ -0,0 +1,338 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
import android.content.Context
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputEditText
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterPhoneNumberV2Binding
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.util.CountryPrefix
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2State
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
import org.thoughtcrime.securesms.registration.v2.ui.toE164
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.livedata.LiveDataObserverCallback
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
/**
* Screen in registration where the user enters their phone number.
*/
class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registration_enter_phone_number_v2) {
private val TAG = Log.tag(EnterPhoneNumberV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val fragmentViewModel by viewModels<EnterPhoneNumberV2ViewModel>()
private val binding: FragmentRegistrationEnterPhoneNumberV2Binding by ViewBinderDelegate(FragmentRegistrationEnterPhoneNumberV2Binding::bind)
private lateinit var spinnerAdapter: ArrayAdapter<CountryPrefix>
private lateinit var phoneNumberInputLayout: TextInputEditText
private lateinit var spinnerView: MaterialAutoCompleteTextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
popBackStack()
}
}
)
phoneNumberInputLayout = binding.number.editText as TextInputEditText
spinnerView = binding.countryCode.editText as MaterialAutoCompleteTextView
spinnerAdapter = ArrayAdapter<CountryPrefix>(
requireContext(),
R.layout.registration_country_code_dropdown_item,
fragmentViewModel.supportedCountryPrefixes
)
setDebugLogSubmitMultiTapView(binding.verifyHeader)
binding.registerButton.setOnClickListener { onRegistrationButtonClicked() }
binding.toolbar.title = null
val activity = requireActivity() as AppCompatActivity
activity.setSupportActionBar(binding.toolbar)
requireActivity().addMenuProvider(UseProxyMenuProvider(), viewLifecycleOwner)
val existingPhoneNumber = sharedViewModel.uiState.value?.phoneNumber
if (existingPhoneNumber != null) {
fragmentViewModel.restoreState(existingPhoneNumber)
fragmentViewModel.phoneNumber()?.let {
phoneNumberInputLayout.setText(it.nationalNumber.toString())
}
} else if (spinnerView.editableText.isBlank()) {
spinnerView.setText(fragmentViewModel.countryPrefix().toString())
}
sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState ->
presentRegisterButton(sharedState)
presentProgressBar(sharedState.inProgress, sharedState.isReRegister)
if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) {
moveToVerificationEntryScreen()
}
}
fragmentViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
if (fragmentViewModel.isEnteredNumberValid(fragmentState)) {
sharedViewModel.setPhoneNumber(fragmentViewModel.parsePhoneNumber(fragmentState))
} else {
sharedViewModel.setPhoneNumber(null)
}
if (fragmentState.error != EnterPhoneNumberV2State.Error.NONE) {
presentError(fragmentState)
}
}
initializeInputFields()
ViewUtil.focusAndShowKeyboard(phoneNumberInputLayout)
}
private fun initializeInputFields() {
phoneNumberInputLayout.addTextChangedListener {
// TODO [regv2]: country code as you type formatter
fragmentViewModel.setPhoneNumber(it?.toString())
}
phoneNumberInputLayout.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
if (hasFocus) {
binding.scrollView.postDelayed({ binding.scrollView.smoothScrollTo(0, binding.registerButton.bottom) }, 250)
}
}
phoneNumberInputLayout.imeOptions = EditorInfo.IME_ACTION_DONE
phoneNumberInputLayout.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
if (actionId == EditorInfo.IME_ACTION_DONE && v != null) {
onRegistrationButtonClicked()
return@setOnEditorActionListener true
}
false
}
spinnerView.threshold = 100
spinnerView.setAdapter(spinnerAdapter)
spinnerView.addTextChangedListener { s ->
if (s.isNullOrEmpty()) {
return@addTextChangedListener
}
if (s[0] != '+') {
s.insert(0, "+")
}
fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
// TODO [regv2]: setCountryFormatter(it.regionCode)
fragmentViewModel.setCountry(it.digits)
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
phoneNumberInputLayout.setSelection(numberLength, numberLength)
}
}
}
private fun presentRegisterButton(sharedState: RegistrationV2State) {
binding.registerButton.isEnabled = sharedState.phoneNumber != null && PhoneNumberUtil.getInstance().isValidNumber(sharedState.phoneNumber) && !sharedState.inProgress
// TODO [regv2]: always enable the button but display error dialogs if the entered phone number is invalid
}
private fun presentError(state: EnterPhoneNumberV2State) {
when (state.error) {
EnterPhoneNumberV2State.Error.NONE -> {
Unit
}
EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER -> {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(getString(R.string.RegistrationActivity_invalid_number))
setMessage(
String.format(
getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid),
state.phoneNumber
)
)
setPositiveButton(android.R.string.ok) { _, _ -> fragmentViewModel.clearError() }
setOnCancelListener { fragmentViewModel.clearError() }
setOnDismissListener { fragmentViewModel.clearError() }
show()
}
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING -> {
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE -> {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0)?.show()
}
EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT -> {
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
}
}
private fun onRegistrationButtonClicked() {
ViewUtil.hideKeyboard(requireContext(), phoneNumberInputLayout)
sharedViewModel.setInProgress(true)
val hasFcm = validateFcmStatus(requireContext())
if (hasFcm) {
sharedViewModel.uiState.observe(viewLifecycleOwner, FcmTokenRetrievedObserver())
sharedViewModel.fetchFcmToken(requireContext())
} else {
sharedViewModel.setInProgress(false)
// TODO [regv2]: handle if FCM isn't available
}
}
private fun onFcmTokenRetrieved(value: RegistrationV2State) {
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else {
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms)
}
}
private fun presentProgressBar(showProgress: Boolean, isReRegister: Boolean) {
if (showProgress) {
binding.registerButton.setSpinning()
} else {
binding.registerButton.cancelSpinning()
}
binding.countryCode.isEnabled = !showProgress
binding.number.isEnabled = !showProgress
binding.cancelButton.visible = !showProgress && isReRegister
}
private fun validateFcmStatus(context: Context): Boolean {
val fcmStatus = PlayServicesUtil.getPlayServicesStatus(context)
Log.d(TAG, "Got $fcmStatus for Play Services status.")
when (fcmStatus) {
PlayServicesUtil.PlayServicesStatus.SUCCESS -> {
return true
}
PlayServicesUtil.PlayServicesStatus.MISSING -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
return false
}
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_NEEDS_UPDATE)
return false
}
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_TRANSIENT)
return false
}
null -> {
Log.w(TAG, "Null result received from PlayServicesUtil, marking Play Services as missing.")
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.PLAY_SERVICES_MISSING)
return false
}
}
}
private fun onConfirmNumberDialogCanceled() {
Log.d(TAG, "User canceled confirm number, returning to edit number.")
sharedViewModel.setInProgress(false)
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout)
}
private fun presentConfirmNumberDialog(phoneNumber: PhoneNumber, isReRegister: Boolean, canSkipSms: Boolean) {
val title = if (isReRegister) {
R.string.RegistrationActivity_additional_verification_required
} else {
R.string.RegistrationActivity_phone_number_verification_dialog_title
}
val message: CharSequence = SpannableStringBuilder().apply {
append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(phoneNumber.toE164())))
if (!canSkipSms) {
append("\n\n")
append(getString(R.string.RegistrationActivity_a_verification_code_will_be_sent_to_this_number))
}
}
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(title)
setMessage(message)
setPositiveButton(android.R.string.ok) { _, _ ->
Log.d(TAG, "User confirmed number.")
sharedViewModel.onUserConfirmedPhoneNumber(requireContext())
}
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onConfirmNumberDialogCanceled() }
setOnCancelListener { _ -> onConfirmNumberDialogCanceled() }
}.show()
}
private fun moveToVerificationEntryScreen() {
NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode())
sharedViewModel.setInProgress(false)
}
private fun popBackStack() {
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.INITIALIZATION)
NavHostFragment.findNavController(this).popBackStack()
}
private inner class FcmTokenRetrievedObserver : LiveDataObserverCallback<RegistrationV2State>(sharedViewModel.uiState) {
override fun onValue(value: RegistrationV2State): Boolean {
val fcmRetrieved = value.isFcmSupported
if (fcmRetrieved) {
onFcmTokenRetrieved(value)
}
return fcmRetrieved
}
}
private inner class UseProxyMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.enter_phone_number, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.phone_menu_use_proxy) {
NavHostFragment.findNavController(this@EnterPhoneNumberV2Fragment).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEditProxy())
true
} else {
false
}
}
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.registration.util.CountryPrefix
/**
* ViewModel for the phone number entry screen.
*/
class EnterPhoneNumberV2ViewModel : ViewModel() {
private val TAG = Log.tag(EnterPhoneNumberV2ViewModel::class.java)
private val store = MutableStateFlow(EnterPhoneNumberV2State.INIT)
val uiState = store.asLiveData()
val supportedCountryPrefixes: List<CountryPrefix> = PhoneNumberUtil.getInstance().supportedCallingCodes
.map { CountryPrefix(it, PhoneNumberUtil.getInstance().getRegionCodeForCountryCode(it)) }
.sortedBy { it.digits.toString() }
fun countryPrefix(): CountryPrefix {
return supportedCountryPrefixes[store.value.countryPrefixIndex]
}
fun phoneNumber(): PhoneNumber? {
return try {
parsePhoneNumber(store.value)
} catch (ex: NumberParseException) {
Log.w(TAG, "Could not parse phone number in current state.", ex)
null
}
}
fun setPhoneNumber(phoneNumber: String?) {
store.update { it.copy(phoneNumber = phoneNumber ?: "") }
}
fun setCountry(digits: Int) {
val matchingIndex = countryCodeToAdapterIndex(digits)
store.update {
it.copy(countryPrefixIndex = matchingIndex)
}
}
fun parsePhoneNumber(state: EnterPhoneNumberV2State): PhoneNumber {
return PhoneNumberUtil.getInstance().parse(state.phoneNumber, supportedCountryPrefixes[state.countryPrefixIndex].regionCode)
}
fun isEnteredNumberValid(state: EnterPhoneNumberV2State): Boolean {
return try {
PhoneNumberUtil.getInstance().isValidNumber(parsePhoneNumber(state))
} catch (ex: NumberParseException) {
false
}
}
fun restoreState(value: PhoneNumber) {
val prefixIndex = countryCodeToAdapterIndex(value.countryCode)
if (prefixIndex != -1) {
store.update {
it.copy(
countryPrefixIndex = prefixIndex,
phoneNumber = value.nationalNumber.toString()
)
}
}
}
private fun countryCodeToAdapterIndex(countryCode: Int): Int {
return supportedCountryPrefixes.indexOfFirst { prefix -> prefix.digits == countryCode }
}
fun clearError() {
setError(EnterPhoneNumberV2State.Error.NONE)
}
fun setError(error: EnterPhoneNumberV2State.Error) {
store.update {
it.copy(error = error)
}
}
}

View file

@ -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
}

View file

@ -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
)

View file

@ -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)
}
}
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.registration.v2.ui.welcome
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Binding
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions
import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import kotlin.jvm.optionals.getOrNull
/**
* First screen that is displayed on the very first app launch.
*/
class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome_v2) {
private val TAG = Log.tag(WelcomeV2Fragment::class.java)
private val sharedViewModel by activityViewModels<RegistrationV2ViewModel>()
private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
maybePrefillE164()
setDebugLogSubmitMultiTapView(binding.image)
setDebugLogSubmitMultiTapView(binding.title)
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
binding.welcomeTermsButton.setOnClickListener { onTermsClicked() }
binding.welcomeTransferOrRestore.setOnClickListener { onRestoreFromBackupClicked() }
}
private fun onContinueClicked() {
TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true)
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
} else {
skipRestore()
}
}
private fun hasAllPermissions(): Boolean {
val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext())
return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED }
}
private fun skipRestore() {
NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
}
private fun onRestoreFromBackupClicked() {
Toast.makeText(requireContext(), "Not yet implemented.", Toast.LENGTH_SHORT).show()
}
private fun onTermsClicked() {
Toast.makeText(requireContext(), "Not yet implemented.", Toast.LENGTH_SHORT).show()
}
private fun maybePrefillE164() {
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
if (localNumber != null) {
Log.v(TAG, "Phone number detected.")
sharedViewModel.setPhoneNumber(localNumber)
} else {
Log.i(TAG, "Could not read phone number.")
}
} else {
Log.i(TAG, "No phone permission.")
}
}
}

View file

@ -90,9 +90,9 @@ public final class FeatureFlags {
private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList";
private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow";
public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
private static final String PAYPAL_ONE_TIME_DONATIONS = "android.oneTimePayPalDonations.2";
private static final String PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.3";
@ -104,15 +104,15 @@ public final class FeatureFlags {
private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch";
private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode";
private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch";
public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
private static final String PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig";
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback.1";
public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback.1";
public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
private static final String SEPA_DEBIT_DONATIONS = "android.sepa.debit.donations.5";
private static final String IDEAL_DONATIONS = "android.ideal.donations.5";
public static final String IDEAL_ENABLED_REGIONS = "global.donations.idealEnabledRegions";
public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
public static final String IDEAL_ENABLED_REGIONS = "global.donations.idealEnabledRegions";
public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
private static final String CALLING_REACTIONS = "android.calling.reactions";
private static final String NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist";
private static final String CALLING_RAISE_HAND = "android.calling.raiseHand";
@ -127,6 +127,7 @@ public final class FeatureFlags {
private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds";
private static final String MESSAGE_BACKUPS = "android.messageBackups";
private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
private static final String REGISTRATION_V2 = "android.registration.v2";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -209,7 +210,7 @@ public final class FeatureFlags {
);
@VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS);
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2);
/**
* Values in this map will take precedence over any value. This should only be used for local
@ -741,6 +742,11 @@ public final class FeatureFlags {
return getBoolean(CAMERAX_CUSTOM_CONTROLLER, false);
}
/** Whether or not to use the V2 refactor of registration. */
public static boolean registrationV2() {
return getBoolean(REGISTRATION_V2, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View file

@ -0,0 +1,31 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
/**
* A wrapper class that can be implemented in order to create a [LiveData] [Observer] that cleans up after itself.
*
* Useful for one-shot observers that can be executed as a callback on an asynchronous call that updates a [LiveData] upon completion.
*/
abstract class LiveDataObserverCallback<T>(private val liveData: LiveData<T>) : Observer<T> {
final override fun onChanged(value: T) {
val shouldRemove = onValue(value)
if (shouldRemove) {
liveData.removeObserver(this)
}
}
/**
* The body of the observer that gets executed when the value is changed.
* Recommended usage is to check some condition in the [LiveData] to determine whether the data has been handled and therefore can be removed.
*
* @return should remove this observer from the [LiveData]
*/
abstract fun onValue(value: T): Boolean
}

View file

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

View file

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".registration.v2.ui.entercode.EnterCodeV2Fragment">
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".registration.fragments.EnterSmsCodeFragment">
<com.google.android.material.button.MaterialButton
android:id="@+id/wrong_number"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="22dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/registrationactivity_text_view_padding"
android:hyphenationFrequency="normal"
android:maxWidth="150dp"
android:text="@string/RegistrationActivity_wrong_number"
android:textColor="@color/signal_colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/verification_subheader" />
<TextView
android:id="@+id/verify_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:gravity="start"
android:text="@string/RegistrationActivity_verification_code"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/verification_subheader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/verify_header"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:text="@string/RegistrationActivity_enter_the_code_we_sent_to_s" />
<org.thoughtcrime.securesms.components.registration.ActionCountDownButton
android:id="@+id/call_me_count_down"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/code"
android:layout_marginEnd="22dp"
android:layout_marginBottom="8dp"
android:hyphenationFrequency="normal"
android:maxWidth="150dp"
android:maxLines="2"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code"
app:layout_constraintVertical_bias="1.0"
tools:text="@string/RegistrationActivity_call" />
<org.thoughtcrime.securesms.components.registration.ActionCountDownButton
android:id="@+id/resend_sms_count_down"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/code"
android:layout_marginStart="22dp"
android:layout_marginBottom="8dp"
android:hyphenationFrequency="normal"
android:maxWidth="150dp"
android:maxLines="2"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/call_me_count_down"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code"
app:layout_constraintVertical_bias="1.0"
tools:text="@string/RegistrationActivity_resend_code" />
<com.google.android.material.button.MaterialButton
android:id="@+id/having_trouble_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/RegistrationActivity_support_bottom_sheet_title"
android:textColor="@color/signal_colorPrimary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/code" />
<org.thoughtcrime.securesms.components.registration.VerificationCodeView
android:id="@+id/code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wrong_number" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard
android:id="@+id/keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:visibility="visible" />
</LinearLayout>

View file

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context="org.thoughtcrime.securesms.registration.v2.ui.phonenumber.EnterPhoneNumberV2Fragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="32dp"
android:layoutDirection="ltr"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/verify_subheader">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/country_code"
style="@style/Widget.Signal.TextInputLayout.Registration.Dropdown"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:theme="@style/Signal.ThemeOverlay.TextInputLayout"
app:errorEnabled="false"
app:hintEnabled="false">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:digits="+1234567890"
android:drawablePadding="-24dp"
android:hint="@string/Registration_country_code_entry_hint"
android:imeOptions="actionNext"
android:maxLength="4"
android:maxLines="1"
android:padding="0dp"
android:singleLine="true"
tools:text="+1" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/number"
style="@style/Widget.Signal.TextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/RegistrationActivity_phone_number_description"
app:materialThemeOverlay="@style/Signal.ThemeOverlay.TextInputLayout">
<com.google.android.material.textfield.TextInputEditText
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="phone">
<requestFocus />
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<TextView
android:id="@+id/verify_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="24dp"
android:text="@string/RegistrationActivity_phone_number"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="16dp"
android:text="@string/RegistrationActivity_enter_your_phone_number_to_get_started"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:layout_editor_absoluteX="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/registerButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
app:circularProgressMaterialButton__label="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toTopOf="@+id/cancel_button"
app:layout_constraintEnd_toEndOf="parent"
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
android:importantForAccessibility="no"
android:src="@drawable/welcome"
app:layout_constraintBottom_toTopOf="@+id/title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:gravity="center"
android:text="@string/RegistrationActivity_take_privacy_with_you_be_yourself_in_every_message"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintBottom_toTopOf="@+id/welcome_terms_button"
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/welcome_terms_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="@string/RegistrationActivity_terms_and_privacy"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toTopOf="@+id/welcome_continue_button"
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/welcome_continue_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="17dp"
app:circularProgressMaterialButton__label="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toTopOf="@id/welcome_transfer_or_restore"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin"
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" />
<com.google.android.material.button.MaterialButton
android:id="@+id/welcome_transfer_or_restore"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="24dp"
android:text="@string/registration_activity__transfer_or_restore_account"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/welcome_continue_button"
app:layout_constraintStart_toStartOf="@+id/welcome_continue_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,492 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signup_v2"
app:startDestination="@id/welcomeV2Fragment">
<fragment
android:id="@+id/welcomeV2Fragment"
android:name="org.thoughtcrime.securesms.registration.v2.ui.welcome.WelcomeV2Fragment"
android:label="fragment_welcome"
tools:layout="@layout/fragment_registration_welcome_v2">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_transfer_or_restore"
app:destination="@id/transferOrRestore"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_welcomeFragment_to_deviceTransferSetup"
app:destination="@id/deviceTransferSetup"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_welcomeFragment_to_grantPermissionsV2Fragment"
app:destination="@id/grantPermissionsV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/grantPermissionsV2Fragment"
android:name="org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment"
android:label="fragment_grant_permissions">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_transfer_or_restore"
app:destination="@id/transferOrRestore"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<argument
android:name="welcomeAction"
app:argType="org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment$WelcomeAction" />
</fragment>
<fragment
android:id="@+id/chooseBackupV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.ChooseBackupFragment"
android:label="fragment_choose_backup"
tools:layout="@layout/fragment_registration_choose_backup">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/chooseBackupV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
</fragment>
<fragment
android:id="@+id/enterPhoneNumberV2Fragment"
android:name="org.thoughtcrime.securesms.registration.v2.ui.phonenumber.EnterPhoneNumberV2Fragment"
android:label="fragment_enter_phone_number"
tools:layout="@layout/fragment_registration_enter_phone_number_v2">
<action
android:id="@+id/action_pickCountry"
app:destination="@id/countryPickerV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_enterVerificationCode"
app:destination="@id/enterCodeV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_editProxy"
app:destination="@+id/registrationProxyV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_reRegisterWithPinV2Fragment"
app:destination="@id/reRegisterWithPinV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/enterPhoneNumberV2Fragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/countryPickerV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
android:label="fragment_country_picker"
tools:layout="@layout/fragment_registration_country_picker" />
<fragment
android:id="@+id/enterCodeV2Fragment"
android:name="org.thoughtcrime.securesms.registration.v2.ui.entercode.EnterCodeV2Fragment"
android:label="fragment_enter_code"
tools:layout="@layout/fragment_registration_enter_code_v2">
<action
android:id="@+id/action_requireKbsLockPin"
app:destination="@id/registrationLockV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderV2Fragment"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_accountLocked"
app:destination="@id/accountLockedV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/registrationLockV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationLockFragment"
android:label="fragment_kbs_lock"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_accountLocked"
app:destination="@id/accountLockedV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
<argument
android:name="timeRemaining"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/reRegisterWithPinV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderV2Fragment"
app:destination="@id/registrationCompletePlaceHolderV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_reRegisterWithPinFragment_to_enterPhoneNumberV2Fragment"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/reRegisterWithPinV2Fragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/accountLockedV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
android:label="fragment_account_locked"
tools:layout="@layout/account_locked_fragment" />
<fragment
android:id="@+id/captchaV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
android:label="fragment_captcha"
tools:layout="@layout/fragment_registration_captcha" />
<fragment
android:id="@+id/restoreBackupV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment"
android:label="fragment_restore_backup"
tools:layout="@layout/fragment_registration_restore_backup">
<action
android:id="@+id/action_backupRestored"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
<action
android:id="@+id/action_noBackupFound"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupV2Fragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip_no_return"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/restoreBackupV2Fragment"
app:popUpToInclusive="true" />
<argument
android:name="uri"
android:defaultValue="@null"
app:argType="android.net.Uri"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/registrationCompletePlaceHolderV2Fragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationCompleteFragment"
android:label="fragment_registration_complete_place_holder"
tools:layout="@layout/fragment_registration_blank" />
<fragment
android:id="@+id/registrationProxyV2Fragment"
android:name="org.thoughtcrime.securesms.preferences.EditProxyFragment"
android:label="fragment_registration_edit_proxy"
tools:layout="@layout/edit_proxy_fragment" />
<fragment
android:id="@+id/transferOrRestore"
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.TransferOrRestoreFragment"
tools:layout="@layout/fragment_transfer_restore">
<action
android:id="@+id/action_choose_backup"
app:destination="@id/chooseBackupV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_new_device_transfer_instructions"
app:destination="@id/newDeviceTransferInstructions"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_transferOrRestore_to_moreOptions"
app:destination="@+id/transferOrRestoreMoreOptionsDialog" />
</fragment>
<fragment
android:id="@+id/restoreFromBackupV2Fragment"
android:name="org.thoughtcrime.securesms.backup.v2.ui.restore.RestoreFromBackupFragment">
<action
android:id="@+id/action_restoreFromBacakupFragment_to_moreOptions"
app:destination="@+id/transferOrRestoreMoreOptionsDialog" />
<argument
android:name="cancelable"
app:argType="boolean" />
</fragment>
<dialog
android:id="@+id/transferOrRestoreMoreOptionsDialog"
android:name="org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsSheet">
<argument
android:name="mode"
app:argType="org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode" />
</dialog>
<fragment
android:id="@+id/newDeviceTransferInstructions"
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferInstructionsFragment"
tools:layout="@layout/new_device_transfer_instructions_fragment">
<action
android:id="@+id/action_device_transfer_setup"
app:destination="@id/deviceTransferSetup"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/deviceTransferSetup"
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferSetupFragment"
tools:layout="@layout/device_transfer_setup_fragment">
<action
android:id="@+id/action_new_device_transfer"
app:destination="@id/newDeviceTransfer"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_deviceTransferSetup_to_transferOrRestore"
app:destination="@id/transferOrRestore"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/transferOrRestore"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/newDeviceTransfer"
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferFragment"
tools:layout="@layout/device_transfer_fragment">
<action
android:id="@+id/action_newDeviceTransfer_to_newDeviceTransferInstructions"
app:destination="@id/newDeviceTransferInstructions"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/newDeviceTransferInstructions"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_newDeviceTransfer_to_newDeviceTransferComplete"
app:destination="@id/newDeviceTransferComplete"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/welcomeV2Fragment" />
</fragment>
<fragment
android:id="@+id/newDeviceTransferComplete"
android:name="org.thoughtcrime.securesms.devicetransfer.newdevice.NewDeviceTransferCompleteFragment"
tools:layout="@layout/new_device_transfer_complete_fragment">
<action
android:id="@+id/action_newDeviceTransferComplete_to_enterPhoneNumberV2Fragment"
app:destination="@id/enterPhoneNumberV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<action
android:id="@+id/action_restart_to_welcomeV2Fragment"
app:destination="@id/welcomeV2Fragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/welcomeV2Fragment"
app:popUpToInclusive="true" />
</navigation>

View file

@ -13,7 +13,6 @@ import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.signal.libsignal.usernames.Username.UsernameLink;
@ -47,6 +46,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.CdsiV2Service;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
@ -219,7 +219,7 @@ public class SignalServiceAccountManager {
public ServiceResponse<RegistrationSessionMetadataResponse> createRegistrationSession(@Nullable String fcmToken, @Nullable String mcc, @Nullable String mnc) {
try {
final RegistrationSessionMetadataResponse response = pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc);
final RegistrationSessionMetadataResponse response = pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc);
return ServiceResponse.forResult(response, 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
@ -311,6 +311,10 @@ public class SignalServiceAccountManager {
}
}
public @Nonnull VerifyAccountResponse registerAccountV2(@Nullable String sessionId, @Nullable String recoveryPassword, AccountAttributes attributes, PreKeyCollection aciPreKeys, PreKeyCollection pniPreKeys, String fcmToken, boolean skipDeviceTransfer) throws IOException {
return pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer);
}
public @Nonnull ServiceResponse<VerifyAccountResponse> changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) {
try {
VerifyAccountResponse response = this.pushServiceSocket.changeNumber(changePhoneNumberRequest);
@ -870,6 +874,10 @@ public class SignalServiceAccountManager {
return KeysApi.create(pushServiceSocket);
}
public RegistrationApi getRegistrationApi() {
return new RegistrationApi(pushServiceSocket);
}
public AuthCredentials getPaymentsAuthorization() throws IOException {
return pushServiceSocket.getPaymentsAuthorization();
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.registration
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.account.AccountAttributes
import org.whispersystems.signalservice.api.account.PreKeyCollection
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.Locale
/**
* Class to interact with various registration-related endpoints.
*/
class RegistrationApi(
private val pushServiceSocket: PushServiceSocket
) {
/**
* Request that the service initialize a new registration session.
*/
fun createRegistrationSession(fcmToken: String?, mcc: String?, mnc: String?): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.createVerificationSession(fcmToken, mcc, mnc)
}
}
/**
* Submit an FCM token to the service as proof that this is an honest user attempting to register.
*/
fun submitPushChallengeToken(sessionId: String?, pushChallengeToken: String?): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.patchVerificationSession(sessionId, null, null, null, null, pushChallengeToken)
}
}
/**
* Request an SMS verification code. On success, the server will send
* an SMS verification code to this Signal user.
*
* @param androidSmsRetrieverSupported whether the system framework will automatically parse the incoming verification message.
*/
fun requestSmsVerificationCode(sessionId: String?, locale: Locale?, androidSmsRetrieverSupported: Boolean): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.requestVerificationCode(sessionId, locale, androidSmsRetrieverSupported, PushServiceSocket.VerificationCodeTransport.SMS)
}
}
/**
* Submit a verification code sent by the service via one of the supported channels (SMS, phone call) to prove the registrant's control of the phone number.
*/
fun verifyAccount(verificationCode: String, sessionId: String): NetworkResult<RegistrationSessionMetadataResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.submitVerificationCode(sessionId, verificationCode)
}
}
/**
* Submit the cryptographic assets required for an account to use the service.
*/
fun registerAccount(sessionId: String?, recoveryPassword: String?, attributes: AccountAttributes?, aciPreKeys: PreKeyCollection?, pniPreKeys: PreKeyCollection?, fcmToken: String?, skipDeviceTransfer: Boolean): NetworkResult<VerifyAccountResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer)
}
}
}

View file

@ -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)