mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Add developer only crude link device UX.
This commit is contained in:
@@ -238,6 +238,7 @@ android {
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
@@ -317,6 +318,7 @@ android {
|
||||
isMinifyEnabled = false
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Spinner\"")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("perf") {
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.link
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCode
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.shared.RegistrationScreen
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
* Crude show QR code on link device to allow linking from primary device.
|
||||
*/
|
||||
class RegisterLinkDeviceQrFragment : ComposeFragment() {
|
||||
|
||||
private val sharedViewModel by activityViewModels<RegistrationViewModel>()
|
||||
private val viewModel: RegisterLinkDeviceQrViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
})
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel
|
||||
.state
|
||||
.mapNotNull { it.provisionMessage }
|
||||
.distinctUntilChanged()
|
||||
.collect { message ->
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = sharedViewModel.registerAsLinkedDevice(requireContext().applicationContext, message)
|
||||
|
||||
when (result) {
|
||||
RegisterLinkDeviceResult.Success -> Unit
|
||||
else -> viewModel.setRegisterAsLinkedDeviceError(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
RegisterLinkDeviceQrScreen(
|
||||
state = state,
|
||||
onRetryQrCode = viewModel::restartProvisioningSocket,
|
||||
onErrorDismiss = viewModel::clearErrors,
|
||||
onCancel = { findNavController().popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RegisterLinkDeviceQrScreen(
|
||||
state: RegisterLinkDeviceQrViewModel.RegisterLinkDeviceState,
|
||||
onRetryQrCode: () -> Unit = {},
|
||||
onErrorDismiss: () -> Unit = {},
|
||||
onCancel: () -> Unit = {}
|
||||
) {
|
||||
// TODO [link-device] use actual design
|
||||
RegistrationScreen(
|
||||
title = "Scan this code with your phone",
|
||||
subtitle = null,
|
||||
bottomContent = {
|
||||
TextButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(space = 48.dp, alignment = Alignment.CenterHorizontally),
|
||||
verticalArrangement = Arrangement.spacedBy(space = 48.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalGutters()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.widthIn(160.dp, 320.dp)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(SignalTheme.colors.colorSurface5)
|
||||
.padding(40.dp)
|
||||
) {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = state.qrState,
|
||||
contentKey = { it::class },
|
||||
contentAlignment = Alignment.Center,
|
||||
label = "qr-code-progress",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) { qrState ->
|
||||
when (qrState) {
|
||||
is RegisterLinkDeviceQrViewModel.QrState.Loaded -> {
|
||||
QrCode(
|
||||
data = qrState.qrData,
|
||||
foregroundColor = Color(0xFF2449C0),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
|
||||
RegisterLinkDeviceQrViewModel.QrState.Loading -> {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||
}
|
||||
}
|
||||
|
||||
is RegisterLinkDeviceQrViewModel.QrState.Scanned,
|
||||
RegisterLinkDeviceQrViewModel.QrState.Failed -> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val text = if (state.qrState is RegisterLinkDeviceQrViewModel.QrState.Scanned) {
|
||||
"Scanned on device"
|
||||
} else {
|
||||
stringResource(R.string.RestoreViaQr_qr_code_error)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.Small(
|
||||
onClick = onRetryQrCode
|
||||
) {
|
||||
Text(text = stringResource(R.string.RestoreViaQr_retry))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [link-device] use actual copy
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterVertically)
|
||||
.widthIn(160.dp, 320.dp)
|
||||
) {
|
||||
InstructionRow(
|
||||
icon = painterResource(R.drawable.symbol_settings_android_24),
|
||||
instruction = "Open Signal Settings on your device"
|
||||
)
|
||||
|
||||
InstructionRow(
|
||||
icon = painterResource(R.drawable.symbol_link_24),
|
||||
instruction = "Tap \"Linked devices\""
|
||||
)
|
||||
|
||||
InstructionRow(
|
||||
icon = painterResource(R.drawable.symbol_qrcode_24),
|
||||
instruction = "Tap \"Link a new device\" and scan this code"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isRegistering) {
|
||||
Dialogs.IndeterminateProgressDialog()
|
||||
} else if (state.showProvisioningError) {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = "failed provision",
|
||||
onDismiss = onErrorDismiss,
|
||||
dismiss = stringResource(android.R.string.ok)
|
||||
)
|
||||
} else if (state.registrationErrorResult != null) {
|
||||
val message = when (state.registrationErrorResult) {
|
||||
RegisterLinkDeviceResult.IncorrectVerification -> "incorrect verification"
|
||||
RegisterLinkDeviceResult.InvalidRequest -> "invalid request"
|
||||
RegisterLinkDeviceResult.MaxLinkedDevices -> "max linked devices reached"
|
||||
RegisterLinkDeviceResult.MissingCapability -> "missing capability, must update"
|
||||
is RegisterLinkDeviceResult.NetworkException -> "network exception ${state.registrationErrorResult.t.message}"
|
||||
is RegisterLinkDeviceResult.RateLimited -> "rate limited ${state.registrationErrorResult.retryAfter}"
|
||||
is RegisterLinkDeviceResult.UnexpectedException -> "unexpected exception ${state.registrationErrorResult.t.message}"
|
||||
RegisterLinkDeviceResult.Success -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
Dialogs.SimpleMessageDialog(
|
||||
message = message,
|
||||
onDismiss = onErrorDismiss,
|
||||
dismiss = stringResource(android.R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstructionRow(
|
||||
icon: Painter,
|
||||
instruction: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
text = instruction,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun InstructionRowPreview() {
|
||||
Previews.Preview {
|
||||
InstructionRow(
|
||||
icon = painterResource(R.drawable.symbol_phone_24),
|
||||
instruction = "Instruction!"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.registrationv3.ui.link
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket
|
||||
import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher
|
||||
import org.whispersystems.signalservice.internal.push.ProvisionMessage
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* Handles creating and maintaining a provisioning websocket in the pursuit
|
||||
* of adding this device as a linked device.
|
||||
*/
|
||||
class RegisterLinkDeviceQrViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RegisterLinkDeviceQrViewModel::class)
|
||||
}
|
||||
|
||||
private val store: MutableStateFlow<RegisterLinkDeviceState> = MutableStateFlow(RegisterLinkDeviceState())
|
||||
|
||||
val state: StateFlow<RegisterLinkDeviceState> = store
|
||||
|
||||
private var socketHandles: MutableList<Closeable> = mutableListOf()
|
||||
private var startNewSocketJob: Job? = null
|
||||
|
||||
init {
|
||||
restartProvisioningSocket()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
shutdown()
|
||||
}
|
||||
|
||||
fun restartProvisioningSocket() {
|
||||
shutdown()
|
||||
|
||||
startNewSocket()
|
||||
|
||||
startNewSocketJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
var count = 0
|
||||
while (count < 5 && isActive) {
|
||||
delay(ProvisioningSocket.LIFESPAN / 2)
|
||||
if (isActive) {
|
||||
startNewSocket()
|
||||
count++
|
||||
Log.d(TAG, "Started next websocket count: $count")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNewSocket() {
|
||||
synchronized(socketHandles) {
|
||||
socketHandles += start()
|
||||
|
||||
if (socketHandles.size > 2) {
|
||||
socketHandles.removeAt(0).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun start(): Closeable {
|
||||
store.update {
|
||||
if (it.qrState !is QrState.Loaded) {
|
||||
it.copy(qrState = QrState.Loading)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
return ProvisioningSocket.start<ProvisionMessage>(
|
||||
mode = ProvisioningSocket.Mode.LINK,
|
||||
identityKeyPair = IdentityKeyUtil.generateIdentityKeyPair(),
|
||||
configuration = AppDependencies.signalServiceNetworkAccess.getConfiguration(),
|
||||
handler = { id, t ->
|
||||
store.update {
|
||||
if (it.currentSocketId == null || it.currentSocketId == id) {
|
||||
Log.w(TAG, "Current socket [$id] has failed, stopping automatic connects", t)
|
||||
shutdown()
|
||||
it.copy(currentSocketId = null, qrState = QrState.Failed)
|
||||
} else {
|
||||
Log.i(TAG, "Old socket [$id] failed, ignoring")
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
) { socket ->
|
||||
val url = socket.getProvisioningUrl()
|
||||
store.update {
|
||||
Log.d(TAG, "Updating QR code with data from [${socket.id}]")
|
||||
|
||||
it.copy(
|
||||
currentSocketId = socket.id,
|
||||
qrState = QrState.Loaded(
|
||||
qrData = QrCodeData.forData(
|
||||
data = url,
|
||||
supportIconOverlay = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val result = socket.getProvisioningMessageDecryptResult()
|
||||
|
||||
if (result is SecondaryProvisioningCipher.ProvisioningDecryptResult.Success) {
|
||||
store.update { it.copy(isRegistering = true, provisionMessage = result.message, qrState = QrState.Scanned) }
|
||||
shutdown()
|
||||
} else {
|
||||
store.update {
|
||||
if (it.currentSocketId == socket.id) {
|
||||
it.copy(qrState = QrState.Scanned, showProvisioningError = true)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
startNewSocketJob?.cancel()
|
||||
synchronized(socketHandles) {
|
||||
socketHandles.forEach { it.close() }
|
||||
socketHandles.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearErrors() {
|
||||
store.update {
|
||||
it.copy(
|
||||
showProvisioningError = false,
|
||||
registrationErrorResult = null
|
||||
)
|
||||
}
|
||||
|
||||
restartProvisioningSocket()
|
||||
}
|
||||
|
||||
fun setRegisterAsLinkedDeviceError(result: RegisterLinkDeviceResult) {
|
||||
store.update {
|
||||
it.copy(registrationErrorResult = result)
|
||||
}
|
||||
}
|
||||
|
||||
data class RegisterLinkDeviceState(
|
||||
val isRegistering: Boolean = false,
|
||||
val qrState: QrState = QrState.Loading,
|
||||
val provisionMessage: ProvisionMessage? = null,
|
||||
val showProvisioningError: Boolean = false,
|
||||
val registrationErrorResult: RegisterLinkDeviceResult? = null,
|
||||
val currentSocketId: Int? = null
|
||||
)
|
||||
|
||||
sealed interface QrState {
|
||||
data object Loading : QrState
|
||||
data class Loaded(val qrData: QrCodeData) : QrState
|
||||
data object Failed : QrState
|
||||
data object Scanned : QrState
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -52,6 +54,17 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
binding.welcomeTransferOrRestore.setOnClickListener { onRestoreOrTransferClicked() }
|
||||
binding.welcomeTransferOrRestore.visible = !sharedViewModel.isReregister
|
||||
|
||||
if (BuildConfig.LINK_DEVICE_UX_ENABLED) {
|
||||
binding.image.setOnLongClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage("Link device?")
|
||||
.setPositiveButton("Link", { _, _ -> onLinkDeviceClicked() })
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
childFragmentManager.setFragmentResultListener(RestoreWelcomeBottomSheet.REQUEST_KEY, viewLifecycleOwner) { requestKey, bundle ->
|
||||
if (requestKey == RestoreWelcomeBottomSheet.REQUEST_KEY) {
|
||||
when (val userSelection = bundle.getSerializableCompat(RestoreWelcomeBottomSheet.REQUEST_KEY, WelcomeUserSelection::class.java)) {
|
||||
@@ -69,6 +82,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
WelcomeUserSelection.RESTORE_WITH_OLD_PHONE,
|
||||
WelcomeUserSelection.RESTORE_WITH_NO_PHONE -> navigateToNextScreenViaRestore(userSelection)
|
||||
WelcomeUserSelection.CONTINUE -> navigateToNextScreenViaContinue()
|
||||
WelcomeUserSelection.LINK -> navigateToLinkDevice()
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
@@ -76,6 +90,18 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLinkDeviceClicked() {
|
||||
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.actionWelcomeFragmentToGrantPermissionsFragment(WelcomeUserSelection.LINK))
|
||||
} else {
|
||||
navigateToLinkDevice()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToLinkDevice() {
|
||||
findNavController().safeNavigate(WelcomeFragmentDirections.goToLinkViaQr())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sharedViewModel.resetRestoreDecision()
|
||||
@@ -115,6 +141,7 @@ class WelcomeFragment : LoggingFragment(R.layout.fragment_registration_welcome_v
|
||||
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
|
||||
|
||||
when (userSelection) {
|
||||
WelcomeUserSelection.LINK,
|
||||
WelcomeUserSelection.CONTINUE -> throw IllegalArgumentException()
|
||||
WelcomeUserSelection.RESTORE_WITH_OLD_PHONE -> {
|
||||
sharedViewModel.intendToRestore(hasOldDevice = true, fromRemote = true)
|
||||
|
||||
@@ -9,5 +9,5 @@ package org.thoughtcrime.securesms.registrationv3.ui.welcome
|
||||
* User options available to start registration flow.
|
||||
*/
|
||||
enum class WelcomeUserSelection {
|
||||
CONTINUE, RESTORE_WITH_OLD_PHONE, RESTORE_WITH_NO_PHONE
|
||||
CONTINUE, RESTORE_WITH_OLD_PHONE, RESTORE_WITH_NO_PHONE, LINK
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ object RemoteConfig {
|
||||
hotSwappable = false,
|
||||
active = false
|
||||
) { value ->
|
||||
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false)
|
||||
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || BuildConfig.LINK_DEVICE_UX_ENABLED || value.asBoolean(false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
<action
|
||||
android:id="@+id/go_to_linkViaQr"
|
||||
app:destination="@id/linkViaQr"
|
||||
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
|
||||
@@ -81,6 +89,11 @@
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/linkViaQr"
|
||||
android:name="org.thoughtcrime.securesms.registrationv3.ui.link.RegisterLinkDeviceQrFragment">
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/noBackupToRestore"
|
||||
android:name="org.thoughtcrime.securesms.registrationv3.ui.restore.NoBackupToRestoreFragment">
|
||||
|
||||
Reference in New Issue
Block a user