Update add linked devices screen.

This commit is contained in:
Michelle Tang 2024-06-05 18:14:17 -07:00 committed by Alex Hart
parent ac52b5b992
commit d3eb480d31
19 changed files with 1429 additions and 28 deletions

View file

@ -540,6 +540,7 @@ dependencies {
}
implementation(libs.stream)
implementation(libs.lottie)
implementation(libs.lottie.compose)
implementation(libs.signal.android.database.sqlcipher)
implementation(libs.androidx.sqlite)
implementation(libs.google.ez.vcard) {

View file

@ -0,0 +1,204 @@
package org.thoughtcrime.securesms.linkdevice
import android.Manifest
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
import org.thoughtcrime.securesms.BiometricDeviceLockContract
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Fragment that allows users to scan a QR code from their camera to link a device
*/
class AddLinkDeviceFragment : ComposeFragment() {
companion object {
private val TAG = Log.tag(AddLinkDeviceFragment::class)
}
private val viewModel: LinkDeviceViewModel by activityViewModels()
private lateinit var biometricAuth: BiometricDeviceAuthentication
private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher<String>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int ->
if (result == BiometricDeviceAuthentication.AUTHENTICATED) {
viewModel.addDevice()
} else {
viewModel.clearBiometrics()
}
}
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(requireContext().getString(R.string.BiometricDeviceAuthentication__signal))
.setConfirmationRequired(true)
.build()
biometricAuth = BiometricDeviceAuthentication(
BiometricManager.from(requireActivity()),
BiometricPrompt(requireActivity(), BiometricAuthenticationListener()),
promptInfo
)
}
override fun onPause() {
super.onPause()
viewModel.clearBiometrics()
biometricAuth.cancelAuthentication()
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsState()
val navController: NavController by remember { mutableStateOf(findNavController()) }
val cameraPermissionState: PermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
if (!state.seenIntroSheet) {
navController.safeNavigate(R.id.action_addLinkDeviceFragment_to_linkDeviceIntroBottomSheet)
viewModel.markIntroSheetSeen()
}
if ((state.qrCodeFound || state.qrCodeInvalid) && navController.currentDestination?.id == R.id.linkDeviceIntroBottomSheet) {
navController.popBackStack()
}
MainScreen(
state = state,
navController = navController,
hasPermissions = cameraPermissionState.status.isGranted,
onRequestPermissions = { askPermissions() },
onShowFrontCamera = { viewModel.showFrontCamera() },
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
onQrCodeApproved = {
viewModel.onQrCodeApproved()
biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher.launch(getString(R.string.BiometricDeviceAuthentication__signal)) }
},
onQrCodeDismissed = { viewModel.onQrCodeDismissed() },
onQrCodeRetry = { viewModel.onQrCodeScanned(state.url) },
onLinkDeviceSuccess = {
viewModel.onLinkDeviceResult(true)
navController.popBackStack()
},
onLinkDeviceFailure = { viewModel.onLinkDeviceResult(false) }
)
}
private fun askPermissions() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager)
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() }
.execute()
}
@SuppressLint("MissingSuperCall")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
private inner class BiometricAuthenticationListener : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) {
Log.w(TAG, "Linked device authentication error: $errorCode")
viewModel.clearBiometrics()
onAuthenticationFailed()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.i(TAG, "Linked device authentication succeeded")
viewModel.addDevice()
}
override fun onAuthenticationFailed() {
Log.w(TAG, "Linked device unable to authenticate")
}
}
}
@Composable
private fun MainScreen(
state: LinkDeviceSettingsState,
navController: NavController? = null,
hasPermissions: Boolean = false,
onRequestPermissions: () -> Unit = {},
onShowFrontCamera: () -> Unit = {},
onQrCodeScanned: (String) -> Unit = {},
onQrCodeApproved: () -> Unit = {},
onQrCodeDismissed: () -> Unit = {},
onQrCodeRetry: () -> Unit = {},
onLinkDeviceSuccess: () -> Unit = {},
onLinkDeviceFailure: () -> Unit = {}
) {
Scaffolds.Settings(
title = "",
onNavigationClick = { navController?.popBackStack() },
navigationIconPainter = painterResource(id = R.drawable.ic_x),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
actions = {
IconButton(onClick = { onShowFrontCamera() }) {
Icon(painterResource(id = R.drawable.symbol_switch_24), contentDescription = null)
}
}
) { contentPadding: PaddingValues ->
LinkDeviceQrScanScreen(
hasPermission = hasPermissions,
onRequestPermissions = onRequestPermissions,
showFrontCamera = state.showFrontCamera,
qrCodeFound = state.qrCodeFound,
qrCodeInvalid = state.qrCodeInvalid,
onQrCodeScanned = onQrCodeScanned,
onQrCodeAccepted = onQrCodeApproved,
onQrCodeDismissed = onQrCodeDismissed,
onQrCodeRetry = onQrCodeRetry,
pendingBiometrics = state.pendingBiometrics,
linkDeviceResult = state.linkDeviceResult,
onLinkDeviceSuccess = onLinkDeviceSuccess,
onLinkDeviceFailure = onLinkDeviceFailure,
modifier = Modifier.padding(contentPadding)
)
}
}
@SignalPreview
@Composable
private fun LinkDeviceAddScreenPreview() {
Previews.Preview {
MainScreen(
state = LinkDeviceSettingsState()
)
}
}

View file

@ -0,0 +1,78 @@
package org.thoughtcrime.securesms.linkdevice
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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 org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Bottom sheet dialog prompting users to name their newly linked device
*/
class LinkDeviceFinishedSheet : ComposeBottomSheetDialogFragment() {
@Composable
override fun SheetContent() {
FinishedSheet(this::dismissAllowingStateLoss)
}
}
@Composable
fun FinishedSheet(onClick: () -> Unit) {
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
.padding(16.dp)
) {
BottomSheets.Handle()
Icon(
painter = painterResource(R.drawable.ic_devices),
contentDescription = null,
tint = Color.Unspecified
)
Text(
text = stringResource(R.string.AddLinkDeviceFragment__finish_linking_on_other_device),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 12.dp, horizontal = 12.dp)
)
Text(
text = stringResource(R.string.AddLinkDeviceFragment__finish_linking_signal),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 12.dp)
)
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.defaultMinSize(minWidth = 220.dp).padding(vertical = 20.dp, horizontal = 12.dp)
) {
Text(stringResource(id = R.string.AddLinkDeviceFragment__okay))
}
}
}
@SignalPreview
@Composable
fun FinishedSheetSheetPreview() {
Previews.BottomSheetPreview {
FinishedSheet(onClick = {})
}
}

View file

@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -34,7 +35,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
@ -42,11 +43,11 @@ import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.DeviceActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Locale
/**
@ -54,11 +55,11 @@ import java.util.Locale
*/
class LinkDeviceFragment : ComposeFragment() {
private val viewModel: LinkDeviceViewModel by viewModels()
private val viewModel: LinkDeviceViewModel by activityViewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state
val state by viewModel.state.collectAsState()
LaunchedEffect(state.toastDialog) {
if (state.toastDialog.isNotEmpty()) {
@ -66,6 +67,12 @@ class LinkDeviceFragment : ComposeFragment() {
}
}
LaunchedEffect(state.showFinishedSheet) {
if (state.showFinishedSheet) {
onShowFinishedSheet()
}
}
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__linked_devices),
onNavigationClick = { findNavController().popBackStack() },
@ -93,9 +100,7 @@ class LinkDeviceFragment : ComposeFragment() {
}
private fun openLinkNewDevice() {
// TODO(Michelle): Use linkDeviceAddFragment
startActivity(DeviceActivity.getIntentForScanner(requireContext()))
// findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceAddFragment)
findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_addLinkDeviceFragment)
}
private fun setDeviceToRemove(device: Device?) {
@ -105,6 +110,11 @@ class LinkDeviceFragment : ComposeFragment() {
private fun onRemoveDevice(device: Device) {
viewModel.removeDevice(requireContext(), device)
}
private fun onShowFinishedSheet() {
LinkDeviceFinishedSheet().show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
viewModel.markFinishedSheetSeen()
}
}
@Composable

View file

@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.linkdevice
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Bottom sheet dialog displayed when users click 'Link a device'
*/
class LinkDeviceIntroBottomSheet : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.8f
@Composable
override fun SheetContent() {
EducationSheet(this::dismissAllowingStateLoss)
}
}
@Composable
fun EducationSheet(onClick: () -> Unit) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.linking_device))
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
BottomSheets.Handle()
Box(modifier = Modifier.size(150.dp)) {
LottieAnimation(composition, iterations = LottieConstants.IterateForever, modifier = Modifier.matchParentSize())
}
Text(
text = stringResource(R.string.AddLinkDeviceFragment__scan_qr_code),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 12.dp)
)
Text(
text = stringResource(R.string.AddLinkDeviceFragment__use_this_device_to_scan_qr_code),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 12.dp)
)
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.defaultMinSize(minWidth = 220.dp)
) {
Text(stringResource(id = R.string.AddLinkDeviceFragment__okay))
}
}
}
@SignalPreview
@Composable
fun EducationSheetPreview() {
Previews.BottomSheetPreview {
EducationSheet(onClick = {})
}
}

View file

@ -0,0 +1,118 @@
package org.thoughtcrime.securesms.linkdevice
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import org.signal.core.ui.Dialogs
import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist
import org.thoughtcrime.securesms.qr.QrScanScreens
import java.util.concurrent.TimeUnit
/**
* A screen that allows you to scan a QR code to link a device
*/
@Composable
fun LinkDeviceQrScanScreen(
hasPermission: Boolean,
onRequestPermissions: () -> Unit,
showFrontCamera: Boolean?,
qrCodeFound: Boolean,
qrCodeInvalid: Boolean,
onQrCodeScanned: (String) -> Unit,
onQrCodeAccepted: () -> Unit,
onQrCodeDismissed: () -> Unit,
onQrCodeRetry: () -> Unit,
pendingBiometrics: Boolean,
linkDeviceResult: LinkDeviceRepository.LinkDeviceResult,
onLinkDeviceSuccess: () -> Unit,
onLinkDeviceFailure: () -> Unit,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
if (qrCodeFound) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.DeviceProvisioningActivity_link_this_device),
body = stringResource(id = R.string.AddLinkDeviceFragment__this_device_will_see_your_groups_contacts),
confirm = stringResource(id = R.string.device_list_fragment__link_new_device),
onConfirm = onQrCodeAccepted,
dismiss = stringResource(id = android.R.string.cancel),
onDismiss = onQrCodeDismissed
)
} else if (qrCodeInvalid) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.AddLinkDeviceFragment__linking_device_failed),
body = stringResource(id = R.string.AddLinkDeviceFragment__this_qr_code_not_valid),
confirm = stringResource(id = R.string.AddLinkDeviceFragment__retry),
onConfirm = onQrCodeRetry,
dismiss = stringResource(id = android.R.string.cancel),
onDismiss = onQrCodeDismissed
)
}
LaunchedEffect(linkDeviceResult) {
when (linkDeviceResult) {
LinkDeviceRepository.LinkDeviceResult.SUCCESS -> onLinkDeviceSuccess()
LinkDeviceRepository.LinkDeviceResult.NO_DEVICE -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_no_device, onLinkDeviceFailure)
LinkDeviceRepository.LinkDeviceResult.NETWORK_ERROR -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_network_error, onLinkDeviceFailure)
LinkDeviceRepository.LinkDeviceResult.KEY_ERROR -> makeToast(context, R.string.DeviceProvisioningActivity_content_progress_key_error, onLinkDeviceFailure)
LinkDeviceRepository.LinkDeviceResult.LIMIT_EXCEEDED -> makeToast(context, R.string.DeviceProvisioningActivity_sorry_you_have_too_many_devices_linked_already, onLinkDeviceFailure)
LinkDeviceRepository.LinkDeviceResult.BAD_CODE -> makeToast(context, R.string.DeviceActivity_sorry_this_is_not_a_valid_device_link_qr_code, onLinkDeviceFailure)
LinkDeviceRepository.LinkDeviceResult.UNKNOWN -> Unit
}
}
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
) {
QrScanScreens.QrScanScreen(
factory = { factoryContext ->
val view = QrScannerView(factoryContext)
view.qrData
.throttleFirst(3000, TimeUnit.MILLISECONDS)
.subscribe { data ->
onQrCodeScanned(data)
}
view
},
update = { view: QrScannerView ->
if (pendingBiometrics) {
view.destroy()
} else {
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXModelBlocklist.isBlocklisted())
if (showFrontCamera != null) {
view.toggleCamera()
}
}
},
hasPermission = hasPermission,
onRequestPermissions = onRequestPermissions,
qrString = stringResource(R.string.AddLinkDeviceFragment__scan_the_qr_code)
)
}
}
}
private fun makeToast(context: Context, messageId: Int, onLinkDeviceFailure: () -> Unit) {
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show()
onLinkDeviceFailure()
}

View file

@ -1,15 +1,23 @@
package org.thoughtcrime.securesms.linkdevice
import android.net.Uri
import org.signal.core.util.Base64.decode
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException
import java.io.IOException
import java.security.InvalidKeyException
/**
* Repository for linked devices and its various actions (linking, unlinking, listing).
@ -71,4 +79,49 @@ object LinkDeviceRepository {
}
return defaultDevice
}
fun isValidQr(uri: Uri): Boolean {
val ephemeralId: String? = uri.getQueryParameter("uuid")
val publicKeyEncoded: String? = uri.getQueryParameter("pub_key")
return ephemeralId.isNotNullOrBlank() && publicKeyEncoded.isNotNullOrBlank()
}
fun addDevice(uri: Uri): LinkDeviceResult {
return try {
val accountManager = AppDependencies.signalServiceAccountManager
val verificationCode = accountManager.getNewDeviceVerificationCode()
if (!isValidQr(uri)) {
LinkDeviceResult.BAD_CODE
} else {
val ephemeralId: String? = uri.getQueryParameter("uuid")
val publicKeyEncoded: String? = uri.getQueryParameter("pub_key")
val publicKey = Curve.decodePoint(publicKeyEncoded?.let { decode(it) }, 0)
val aciIdentityKeyPair = SignalStore.account().aciIdentityKey
val pniIdentityKeyPair = SignalStore.account().pniIdentityKey
val profileKey = ProfileKeyUtil.getSelfProfileKey()
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, SignalStore.svr().getOrCreateMasterKey(), verificationCode)
TextSecurePreferences.setMultiDevice(AppDependencies.application, true)
LinkDeviceResult.SUCCESS
}
} catch (e: NotFoundException) {
LinkDeviceResult.NO_DEVICE
} catch (e: DeviceLimitExceededException) {
LinkDeviceResult.LIMIT_EXCEEDED
} catch (e: IOException) {
LinkDeviceResult.NETWORK_ERROR
} catch (e: InvalidKeyException) {
LinkDeviceResult.KEY_ERROR
}
}
enum class LinkDeviceResult {
SUCCESS,
NO_DEVICE,
NETWORK_ERROR,
KEY_ERROR,
LIMIT_EXCEEDED,
BAD_CODE,
UNKNOWN
}
}

View file

@ -9,5 +9,13 @@ data class LinkDeviceSettingsState(
val devices: List<Device> = emptyList(),
val deviceToRemove: Device? = null,
@StringRes val progressDialogMessage: Int = -1,
val toastDialog: String = ""
val toastDialog: String = "",
val showFrontCamera: Boolean? = null,
val qrCodeFound: Boolean = false,
val qrCodeInvalid: Boolean = false,
val url: String = "",
val linkDeviceResult: LinkDeviceRepository.LinkDeviceResult = LinkDeviceRepository.LinkDeviceResult.UNKNOWN,
val showFinishedSheet: Boolean = false,
val seenIntroSheet: Boolean = false,
val pendingBiometrics: Boolean = false
)

View file

@ -1,35 +1,33 @@
package org.thoughtcrime.securesms.linkdevice
import android.content.Context
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
/**
* Maintains the state of the [LinkDeviceFragment]
*/
class LinkDeviceViewModel : ViewModel() {
private val _state = mutableStateOf(LinkDeviceSettingsState())
val state: State<LinkDeviceSettingsState> = _state
fun onResume() {
_state.value = _state.value.copy()
}
private val _state = MutableStateFlow(LinkDeviceSettingsState())
val state = _state.asStateFlow()
fun setDeviceToRemove(device: Device?) {
_state.value = _state.value.copy(deviceToRemove = device)
_state.update { it.copy(deviceToRemove = device) }
}
fun removeDevice(context: Context, device: Device) {
viewModelScope.launch(Dispatchers.IO) {
_state.value = _state.value.copy(
progressDialogMessage = R.string.DeviceListActivity_unlinking_device
)
_state.update { it.copy(progressDialogMessage = R.string.DeviceListActivity_unlinking_device) }
val success = LinkDeviceRepository.removeDevice(device.id)
if (success) {
loadDevices(context)
@ -38,9 +36,9 @@ class LinkDeviceViewModel : ViewModel() {
progressDialogMessage = -1
)
} else {
_state.value = _state.value.copy(
progressDialogMessage = -1
)
_state.update {
it.copy(progressDialogMessage = -1)
}
}
}
}
@ -54,11 +52,120 @@ class LinkDeviceViewModel : ViewModel() {
progressDialogMessage = -1
)
} else {
_state.value = _state.value.copy(
devices = devices,
progressDialogMessage = -1
_state.update {
it.copy(
toastDialog = "",
devices = devices,
progressDialogMessage = -1
)
}
}
}
}
fun showFrontCamera() {
_state.update {
val frontCamera = it.showFrontCamera
it.copy(
showFrontCamera = if (frontCamera == null) true else !frontCamera,
pendingBiometrics = false
)
}
}
fun markIntroSheetSeen() {
_state.update {
it.copy(
seenIntroSheet = true,
showFrontCamera = null
)
}
}
fun onQrCodeScanned(url: String) {
if (_state.value.qrCodeFound || _state.value.qrCodeInvalid) {
return
}
val uri = Uri.parse(url)
if (LinkDeviceRepository.isValidQr(uri)) {
_state.update {
it.copy(
qrCodeFound = true,
qrCodeInvalid = false,
url = url,
showFrontCamera = null
)
}
} else {
_state.update {
it.copy(
qrCodeFound = false,
qrCodeInvalid = true,
url = url,
showFrontCamera = null
)
}
}
}
fun onQrCodeApproved() {
_state.update {
it.copy(
qrCodeFound = false,
qrCodeInvalid = false,
pendingBiometrics = true
)
}
}
fun onQrCodeDismissed() {
_state.update {
it.copy(
qrCodeFound = false,
qrCodeInvalid = false
)
}
}
fun clearBiometrics() {
_state.update {
it.copy(
pendingBiometrics = false
)
}
}
fun addDevice() {
val uri = Uri.parse(_state.value.url)
viewModelScope.launch(Dispatchers.IO) {
val result = LinkDeviceRepository.addDevice(uri)
_state.update {
it.copy(
pendingBiometrics = false,
linkDeviceResult = result,
url = ""
)
}
LinkedDeviceInactiveCheckJob.enqueue()
}
}
fun onLinkDeviceResult(showSheet: Boolean) {
_state.update {
it.copy(
showFinishedSheet = showSheet,
linkDeviceResult = LinkDeviceRepository.LinkDeviceResult.UNKNOWN,
toastDialog = ""
)
}
}
fun markFinishedSheetSeen() {
_state.update {
it.copy(
showFinishedSheet = false
)
}
}
}

View file

@ -0,0 +1,140 @@
package org.thoughtcrime.securesms.qr
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.NoOpUpdate
import org.signal.core.ui.Buttons
import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.R
object QrScanScreens {
/**
* Full-screen qr scanning screen with permission-asking UI
*/
@Composable
fun QrScanScreen(
factory: (Context) -> QrScannerView,
update: (QrScannerView) -> Unit = NoOpUpdate,
hasPermission: Boolean,
onRequestPermissions: () -> Unit = {},
qrString: String
) {
val path = remember { Path() }
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
) {
AndroidView(
factory = factory,
update = update,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.drawWithContent {
drawContent()
if (hasPermission) {
drawQrCrosshair(path)
}
}
)
if (!hasPermission) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center).padding(48.dp)
) {
Text(
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = Color.White
)
Buttons.MediumTonal(
colors = ButtonDefaults.filledTonalButtonColors(),
onClick = onRequestPermissions
) {
Text(stringResource(R.string.CameraXFragment_allow_access))
}
}
} else {
Text(
text = qrString,
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 24.dp).fillMaxWidth()
)
}
}
}
}
private fun DrawScope.drawQrCrosshair(path: Path) {
val crosshairWidth: Float = size.minDimension * 0.6f
val crosshairLineLength = crosshairWidth * 0.125f
val topLeft = center - Offset(crosshairWidth / 2, crosshairWidth / 2)
val topRight = center + Offset(crosshairWidth / 2, -crosshairWidth / 2)
val bottomRight = center + Offset(crosshairWidth / 2, crosshairWidth / 2)
val bottomLeft = center + Offset(-crosshairWidth / 2, crosshairWidth / 2)
path.reset()
drawPath(
path = path.apply {
moveTo(topLeft.x, topLeft.y + crosshairLineLength)
lineTo(topLeft.x, topLeft.y)
lineTo(topLeft.x + crosshairLineLength, topLeft.y)
moveTo(topRight.x - crosshairLineLength, topRight.y)
lineTo(topRight.x, topRight.y)
lineTo(topRight.x, topRight.y + crosshairLineLength)
moveTo(bottomRight.x, bottomRight.y - crosshairLineLength)
lineTo(bottomRight.x, bottomRight.y)
lineTo(bottomRight.x - crosshairLineLength, bottomRight.y)
moveTo(bottomLeft.x + crosshairLineLength, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y)
lineTo(bottomLeft.x, bottomLeft.y - crosshairLineLength)
},
color = Color.White,
style = Stroke(
width = 3.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(10.dp.toPx())
)
)
}
}

View file

@ -235,7 +235,26 @@
<fragment
android:id="@+id/linkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceFragment"
android:label="link_device_fragment" />
android:label="link_device_fragment">
<action
android:id="@+id/action_linkDeviceFragment_to_addLinkDeviceFragment"
app:destination="@id/addLinkDeviceFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/addLinkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.AddLinkDeviceFragment"
android:label="link_device_add_fragment">
<action
android:id="@+id/action_addLinkDeviceFragment_to_linkDeviceIntroBottomSheet"
app:destination="@id/linkDeviceIntroBottomSheet" />
</fragment>
<dialog
android:id="@+id/linkDeviceIntroBottomSheet"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceIntroBottomSheet" />
<!-- Payments -->
<activity

View file

@ -235,7 +235,26 @@
<fragment
android:id="@+id/linkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceFragment"
android:label="link_device_fragment" />
android:label="link_device_fragment">
<action
android:id="@+id/action_linkDeviceFragment_to_addLinkDeviceFragment"
app:destination="@id/addLinkDeviceFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/addLinkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.AddLinkDeviceFragment"
android:label="link_device_add_fragment">
<action
android:id="@+id/action_addLinkDeviceFragment_to_linkDeviceIntroBottomSheet"
app:destination="@id/linkDeviceIntroBottomSheet" />
</fragment>
<dialog
android:id="@+id/linkDeviceIntroBottomSheet"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceIntroBottomSheet" />
<!-- Payments -->
<activity

View file

@ -882,6 +882,28 @@
<!-- Toast message indicating a device has been unlinked where %s is the name of the device -->
<string name="LinkDeviceFragment__s_unlinked">%s unlinked</string>
<!-- AddLinkDeviceFragment -->
<!-- Description text shown on the QR code scanner when linking a device -->
<string name="AddLinkDeviceFragment__scan_the_qr_code">Scan the QR code displayed on the device to link.</string>
<!-- Bottom sheet title telling users to scan a qr code -->
<string name="AddLinkDeviceFragment__scan_qr_code">Scan QR code</string>
<!-- Bottom sheet description telling users how to scan a qr code -->
<string name="AddLinkDeviceFragment__use_this_device_to_scan_qr_code">Use this device to scan the QR code displayed on the device you want to link</string>
<!-- Confirmation button to dismiss bottom sheet dialog -->
<string name="AddLinkDeviceFragment__okay">Okay</string>
<!-- Dialog text describing the consequences of linking a device -->
<string name="AddLinkDeviceFragment__this_device_will_see_your_groups_contacts">This device will be able to see your groups and contacts, access your chats, and send messages in your name.</string>
<!-- Bottom sheet title telling users to complete the linking process on the other device -->
<string name="AddLinkDeviceFragment__finish_linking_on_other_device">Finish linking on your other device</string>
<!-- Bottom sheet description telling users to complete the linking process -->
<string name="AddLinkDeviceFragment__finish_linking_signal">Finish linking Signal on your other device.</string>
<!-- Title of dialog when the QR code being scanned is invalid -->
<string name="AddLinkDeviceFragment__linking_device_failed">Linking device failed</string>
<!-- Text shown in a dialog when the QR code being scanned is invalid -->
<string name="AddLinkDeviceFragment__this_qr_code_not_valid">This QR code is not valid. Please make sure you are scanning the QR code that is displayed on the device you want to link.</string>
<!-- Button in dialog to retry linking a device with a qr code -->
<string name="AddLinkDeviceFragment__retry">Retry</string>
<!-- DeviceListActivity -->
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">By unlinking this device, it will no longer be able to send or receive messages.</string>

View file

@ -148,6 +148,7 @@ dependencyResolutionManagement {
library("android-tooltips", "com.tomergoldst.android:tooltips:1.0.6")
library("stream", "com.annimon:stream:1.1.8")
library("lottie", "com.airbnb.android:lottie:5.2.0")
library("lottie-compose", "com.airbnb.android:lottie-compose:6.4.0")
library("dnsjava", "dnsjava:dnsjava:2.1.9")
library("nanohttpd-webserver", "org.nanohttpd", "nanohttpd-webserver").versionRef("nanohttpd")
library("nanohttpd-websocket", "org.nanohttpd", "nanohttpd-websocket").versionRef("nanohttpd")

View file

@ -142,6 +142,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.7.0">
<artifact name="annotation-1.7.0.module">
<sha256 value="530708656d380605077ea8bca9ae1372fbd1af38e375f411d4e43263bd510c3c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.0.0">
<artifact name="annotation-experimental-1.0.0.aar">
<sha256 value="b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11" origin="Generated by Gradle"/>
@ -171,6 +176,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-experimental" version="1.4.0">
<artifact name="annotation-experimental-1.4.0.aar">
<sha256 value="c6eb7e676011ec65b32428373d450debdfc45179c4f8b3a752174fb87c17b08a" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-experimental-1.4.0.module">
<sha256 value="5930ea7f21fcb6d0deb2ba32748a0ef7c8fd2c42384860582ba7cd20deb90379" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.6.0">
<artifact name="annotation-jvm-1.6.0.jar">
<sha256 value="60b10b5ef5769b79570172e015b8159405c92f034ba88b9391a977589c9deb4e" origin="Generated by Gradle"/>
@ -179,6 +192,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="3f5a8faa19de667e63dca9730ff8ef0e478e4bafb5feeb8258e5c086246dc90c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.7.0">
<artifact name="annotation-jvm-1.7.0.jar">
<sha256 value="e36b8e4b8393a4adc74e3d4ab22ad5a36396f0cea2e40b5734eae14937dfd224" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-jvm-1.7.0.module">
<sha256 value="07ce60c377ab94e47c8c902589b9776030064fd1a7e4d5a01a38d700e35e5db4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.appcompat" name="appcompat" version="1.6.1">
<artifact name="appcompat-1.6.1.aar">
<sha256 value="7ea5573b93ababd3bd32312451c6ea48a662b03a140dda81aebe75776a20a422" origin="Generated by Gradle"/>
@ -379,11 +400,32 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="6c4c0f9e7dab6d983858e67dd99a93af9d9f7d02ac1f8f648ce1fecc84afffa2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.collection" name="collection" version="1.4.0">
<artifact name="collection-1.4.0.module">
<sha256 value="2fd3b523e8276c0254c417b66d8e7fecc5b0b975af5d178afa7f5b813812cfab" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.collection" name="collection-jvm" version="1.4.0">
<artifact name="collection-jvm-1.4.0.jar">
<sha256 value="d5cf7b72647c7995071588fe870450ff9c8f127f253d2d4851e161b800f67ae0" origin="Generated by Gradle"/>
</artifact>
<artifact name="collection-jvm-1.4.0.module">
<sha256 value="21b0b02ea68abe418f3dd4e4d42876ecf3bd9a1cada458b460cae1cd9d58ef6d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.collection" name="collection-ktx" version="1.1.0">
<artifact name="collection-ktx-1.1.0.jar">
<sha256 value="2bfc54475c047131913361f56d0f7f019c6e5bee53eeb0eb7d94a7c499a05227" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.collection" name="collection-ktx" version="1.4.0">
<artifact name="collection-ktx-1.4.0.jar">
<sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/>
</artifact>
<artifact name="collection-ktx-1.4.0.module">
<sha256 value="3770999ec32d1c082d1a34cf1d64d16d9eca9b6b1263c979954407f461fba82b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation" version="1.1.1">
<artifact name="animation-1.1.1.module">
<sha256 value="078b4dcd5f09689281415d9ea0e09d2775d80f016041dacbcee22d54c43a5fa1" origin="Generated by Gradle"/>
@ -405,6 +447,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="28a43ab8fba64e55bcd20126b8ed55f5ab4e8f927f749d46b6fbd1f75af04d7e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation" version="1.6.2">
<artifact name="animation-1.6.2.module">
<sha256 value="1e929c6fdb8ec509a5584e52d3fa11f53353ac204b777790201449aeb9d81e57" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-android" version="1.5.1">
<artifact name="animation-android-1.5.1.module">
<sha256 value="bff0f739876b4ccc9a6add1f5314ae39811a4663613e887b3bfda9eda1f50960" origin="Generated by Gradle"/>
@ -421,6 +468,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="428538775ea55529b953fbb7a6000ccfddc89722c69c7a30431a630fa2fe74f2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-android" version="1.6.2">
<artifact name="animation-android-1.6.2.module">
<sha256 value="2c1f3a50e0d185fdf882c6516254e2d67561ae639bb243a10a77d02c0e7e5f84" origin="Generated by Gradle"/>
</artifact>
<artifact name="animation-release.aar">
<sha256 value="6d620e937f2aa422d5a18595d39b001d9149f6fc0af97d7bed1b1c4dd3a2cae2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-core" version="1.5.1">
<artifact name="animation-core-1.5.1.module">
<sha256 value="cd280d3b8dbf85f251153129cc67f928bc928dfe7e9e410f1858ad331da48b68" origin="Generated by Gradle"/>
@ -437,6 +492,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="747b658ee320f6d55a8d40727d6c6b0d78c1dc899ed66fe8b65adefd783976cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-core" version="1.6.2">
<artifact name="animation-core-1.6.2.module">
<sha256 value="2512e23a51f86a9f51894b9317e3250ee0a0b77eab57e17640ae96b2117bb372" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-core-android" version="1.5.1">
<artifact name="animation-core-android-1.5.1.module">
<sha256 value="dd8fc6cd7d0c0adff3221a007da1ca258510858bbf0b67a61a49da8465009ba4" origin="Generated by Gradle"/>
@ -453,6 +513,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="d7e85fb4fb36db15eb15134ffec8363fda8517ee944af1441815131029f960c8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-core-android" version="1.6.2">
<artifact name="animation-core-android-1.6.2.module">
<sha256 value="58b0948d1ea50b3df8ec1ba7f86355676b715c8f1bb75ac9548eed7709fedd27" origin="Generated by Gradle"/>
</artifact>
<artifact name="animation-core-release.aar">
<sha256 value="d266ed93de86885d2d53017868d9d0e29238663d0f599ef1f094783a0fcbcd26" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-core-desktop" version="1.6.2">
<artifact name="animation-core-desktop-1.6.2.jar">
<sha256 value="791fbb5b90f1db2e128a134d5fb5dfe13b6aa4b184300f351b1466470bff78cc" origin="Generated by Gradle"/>
</artifact>
<artifact name="animation-core-desktop-1.6.2.module">
<sha256 value="78843578abf9d0613ea37e4f1cc7f4c549513c57eaac3cc1eb1dc72af0f7e4e1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.animation" name="animation-desktop" version="1.6.2">
<artifact name="animation-desktop-1.6.2.jar">
<sha256 value="6eb162e32fb6b44cdc50cf3e4ad9906163e35f933cd54f0c03677ce240fe9d7e" origin="Generated by Gradle"/>
</artifact>
<artifact name="animation-desktop-1.6.2.module">
<sha256 value="7ff8993c1c012e1ac273f244b939f7d58d23af80eafc97ed644e3fce0fca94b9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.compiler" name="compiler" version="1.3.2">
<artifact name="compiler-1.3.2.jar">
<sha256 value="d19beb5fc48395e1730acad2f654daa49a17f6b44a2a45a2e23a0dec806a6931" origin="Generated by Gradle"/>
@ -506,6 +590,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="84ba53af9fd289476e76b74216d43ee54006cc587c0fdbec51c8cdf717ab5e7f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation" version="1.6.2">
<artifact name="foundation-1.6.2.module">
<sha256 value="a5c3acf77034297fccadb22af2dadf1f65fc4a4df4381f778ef4133ce241bfd6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-android" version="1.5.1">
<artifact name="foundation-android-1.5.1.module">
<sha256 value="ac397138d4c01416189eea4d519c6b8190eabe96558656fd0ec5f4af7a581708" origin="Generated by Gradle"/>
@ -522,6 +611,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="74075b7f5de082f77ce27b80b8787c59ac3ec86d0a66bf5754377211f2894072" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-android" version="1.6.2">
<artifact name="foundation-android-1.6.2.module">
<sha256 value="f1e3344f6a263ea1db81fb3ebdaa1776fd76a8bba12d06e97f610905fa5e80ce" origin="Generated by Gradle"/>
</artifact>
<artifact name="foundation-release.aar">
<sha256 value="185f6b2cc5a777dc23a6164f5cea08cf9b9089e83754226441ab65b48b19f4dc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-desktop" version="1.6.2">
<artifact name="foundation-desktop-1.6.2.jar">
<sha256 value="1eda12bee5a9660ba703ebb3bb38a2ef57011fdc8f4cbcce0d0d7da02cd0a8d0" origin="Generated by Gradle"/>
</artifact>
<artifact name="foundation-desktop-1.6.2.module">
<sha256 value="a450f21a355c979768f1ca745a687f66cddbb69bddc91031f0207bbfe638f2b5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout" version="1.5.1">
<artifact name="foundation-layout-1.5.1.module">
<sha256 value="9efbdd7abb8bdf1625266a42a560168032d1c0df7046c35b205c2668a57271c7" origin="Generated by Gradle"/>
@ -538,6 +643,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f8cbd92d3585570dce2541aefef0d59b4c591ae225d98921058e484e9abf3e42" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout" version="1.6.2">
<artifact name="foundation-layout-1.6.2.module">
<sha256 value="43f1ce3e2fc8a510b2aaf20439e9e67b416205a61993955cbae8ea618d732cfb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout-android" version="1.5.1">
<artifact name="foundation-layout-android-1.5.1.module">
<sha256 value="4622a385fd5ef110d1cc55e65cf45f3fc98ff63aa2f2c329a35750bf3be4cab0" origin="Generated by Gradle"/>
@ -554,6 +664,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="63d6b3832034e6f187213970f42d82d0ce136d6c6061748f34f05cf2ba1d4364" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout-android" version="1.6.2">
<artifact name="foundation-layout-android-1.6.2.module">
<sha256 value="e6771358144019af6576f2b93479c7635a640a9a20388e815c571a24d8328fad" origin="Generated by Gradle"/>
</artifact>
<artifact name="foundation-layout-release.aar">
<sha256 value="c17f97ecfa5df6020e5b3b6e89c5b617e0ea66cc22ee9a824303fa426a7bc52b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.foundation" name="foundation-layout-desktop" version="1.6.2">
<artifact name="foundation-layout-desktop-1.6.2.jar">
<sha256 value="8bb898232e0d9a8c56de51671769b73ea0a5ab526f9005de5deff34dddf5cbed" origin="Generated by Gradle"/>
</artifact>
<artifact name="foundation-layout-desktop-1.6.2.module">
<sha256 value="a64b0b241ff2ad1c6b1bfffb8a66aa7c5d681f5bc4cc1d71f34b1a5b138f2c30" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material" version="1.5.1">
<artifact name="material-1.5.1.module">
<sha256 value="e409e658869f7923b2f4a9e2963d6727a598489d73a276d1ad26bf082b235826" origin="Generated by Gradle"/>
@ -564,6 +690,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8efb57b6aac3eed66afac34e61facc0795ea1e48ff7b5562cd509fe1496d5a41" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material" version="1.6.2">
<artifact name="material-1.6.2.module">
<sha256 value="873eb66028dac3a9f2cf1a74ab3599cbe4fe6e76d8aae010e4ac63c6c7213bc7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-android" version="1.5.1">
<artifact name="material-android-1.5.1.module">
<sha256 value="04ca13bc09df6e513407c21abb214275d410a3c05c4ee31fabf71981b32c1c16" origin="Generated by Gradle"/>
@ -580,6 +711,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="6ff3b19950724e44348fd122765efdb7f9f58e4c607ca0439f0e15048766b993" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-android" version="1.6.2">
<artifact name="material-android-1.6.2.module">
<sha256 value="6dd6c2ecf69c46162ea244474fb1c0a344462b198ac404a9202c47af9df416fe" origin="Generated by Gradle"/>
</artifact>
<artifact name="material-release.aar">
<sha256 value="ccc622c03a793a5b0244b44469c939ac6286c78760a4d1af503fec9af5074d27" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-icons-core" version="1.5.1">
<artifact name="material-icons-core-1.5.1.module">
<sha256 value="bdeafaa32a4cee70b43cf368b570c11e2786cec5ed83a39b270fcfd2fc5873f6" origin="Generated by Gradle"/>
@ -596,6 +735,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f8cbd92d3585570dce2541aefef0d59b4c591ae225d98921058e484e9abf3e42" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-icons-core" version="1.6.2">
<artifact name="material-icons-core-1.6.2.module">
<sha256 value="8e6968839e23734d5d0375445c23dd9cfe5d9a0fbedc6325ab08e3979f77b503" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-icons-core-android" version="1.5.1">
<artifact name="material-icons-core-android-1.5.1.module">
<sha256 value="2f51667a317486ccc524b4e2e957dde79d267bb86dc71ca5f8e812ca7593538d" origin="Generated by Gradle"/>
@ -612,6 +756,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="fc2bc75dcd539599a773e3a86dbb3a9899fdc58b65e919751f4a2d77bebbd187" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-icons-core-android" version="1.6.2">
<artifact name="material-icons-core-android-1.6.2.module">
<sha256 value="9df684d497b70b0b0ebdb643540d5910e26c26a9084cba864947279a134d5aea" origin="Generated by Gradle"/>
</artifact>
<artifact name="material-icons-core-release.aar">
<sha256 value="2440a3afd96b7af29965f23dc497844360ba1258c09e39cce3a258140a3209f2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-icons-core-desktop" version="1.6.2">
<artifact name="material-icons-core-desktop-1.6.2.jar">
<sha256 value="9ba0b869bf23bd64187c14a72303d604c287d189b095acf2ddd9e772d608f028" origin="Generated by Gradle"/>
</artifact>
<artifact name="material-icons-core-desktop-1.6.2.module">
<sha256 value="224dbceb384a927f973ddbd0973e454cee06c7c0b4b63bb03ca76fc3965e0d99" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-ripple" version="1.5.1">
<artifact name="material-ripple-1.5.1.module">
<sha256 value="0a66c343a196e90c8c4f6338fc93c574090d1e56d88a2bd05c426b196bdd355e" origin="Generated by Gradle"/>
@ -628,6 +788,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="aa1bc0c3320609a2a87a689584e21897b5d7dc971b13b656302ba9caa5b2b5df" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-ripple" version="1.6.2">
<artifact name="material-ripple-1.6.2.module">
<sha256 value="cf3f1909c7d91168fca47bf90ac1546430b6dcc2fb3046dcec9f7a7d23eb2952" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-ripple-android" version="1.5.1">
<artifact name="material-ripple-android-1.5.1.module">
<sha256 value="0ec0d0dd2cab195b1186c8e4df2a61acb2f9f476d84f40823c904977bf6b0e88" origin="Generated by Gradle"/>
@ -644,6 +809,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8a6c8b4bfc8cae9dc7e79653a5de3a482cb9b96d290f6bbdc6d3002b91ed48cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-ripple-android" version="1.6.2">
<artifact name="material-ripple-android-1.6.2.module">
<sha256 value="6869707a87beec8e0d15a305eaed7e843c13098807fe2d9cb79cd1d97dcf2f37" origin="Generated by Gradle"/>
</artifact>
<artifact name="material-ripple-release.aar">
<sha256 value="bf79ee469b54fdf07a594206895791a3d2feafc8d0d1d968482b4c917aee7e4e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material" name="material-ripple-desktop" version="1.6.2">
<artifact name="material-ripple-desktop-1.6.2.jar">
<sha256 value="cd56fbe60dfd619b2990194fe69c711562eae151b7c3e92802878d25088873d0" origin="Generated by Gradle"/>
</artifact>
<artifact name="material-ripple-desktop-1.6.2.module">
<sha256 value="e59f78b136a7eda27cfcccba4c0b2db248bfc35064682526668c5cf41bb62760" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3" name="material3" version="1.1.2">
<artifact name="material3-1.1.2.aar">
<sha256 value="fd9f8fe91d6661afcaf0e9c9cef30b5a196b4def5b28a370f13f2c259c26e482" origin="Generated by Gradle"/>
@ -652,6 +833,27 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c99a42a1546e37872d9a3843b4f9738f66aa93c08a66c54c84f775b3da3e5d97" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3" name="material3" version="1.2.0">
<artifact name="material3-1.2.0.module">
<sha256 value="911db8ed8cd7175beff3ea4f69aab68b5ee6c564959a8a27244c27dbd61d1deb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3" name="material3-android" version="1.2.0">
<artifact name="material3-android-1.2.0.module">
<sha256 value="c2e180d82471b2ff0b3cc0a0ec9f0eed13467ca609f046c9daca8747aa212840" origin="Generated by Gradle"/>
</artifact>
<artifact name="material3-release.aar">
<sha256 value="10facde8f66f91d61a48c8dfece3b493d630822f311cd20643a23416e4f1ea0c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.material3" name="material3-desktop" version="1.2.0">
<artifact name="material3-desktop-1.2.0.jar">
<sha256 value="a94d9a46ce014f0166f4875cd30c55bd6d54733266ce281e0e1ed3028bf8ec45" origin="Generated by Gradle"/>
</artifact>
<artifact name="material3-desktop-1.2.0.module">
<sha256 value="cb5a777ab08f3d0081b12e9f3044e1bcb476d0e09e77d5aae61ea78eef0b90fc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.0.1">
<artifact name="runtime-1.0.1.module">
<sha256 value="2543a8c7edc16bde91f140286b4fd3773d7204a283a4ec99f6e5e286aa92c0c3" origin="Generated by Gradle"/>
@ -681,6 +883,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="1dd30ce8cca076140ed92ee748c1d35a4ac46b9da435406e353208a07267996d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime" version="1.6.2">
<artifact name="runtime-1.6.2.module">
<sha256 value="f201d15cc15e7af4720940328bbdc75aa85d9f25b1a97c2eef7b39767f60e831" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-android" version="1.5.1">
<artifact name="runtime-android-1.5.1.module">
<sha256 value="63d09bac1fe7e3db9a399416e4788dbe0a6b5f79c60b4aa30fec93425ffba9d8" origin="Generated by Gradle"/>
@ -697,6 +904,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="a7df39bcc1327fa5a7ea5dca2976e11b87feea4ca89d1968cbbb6914f983cd4c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-android" version="1.6.2">
<artifact name="runtime-android-1.6.2.module">
<sha256 value="c1fe49667f293b6edba6e93b7465ced8b3f518646ff25e0f7daf089efb4501f7" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-release.aar">
<sha256 value="0363fcf31f147a7d07925818375fc47210676e428c51d168d6bbb4c4f99b0e1a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-desktop" version="1.6.2">
<artifact name="runtime-desktop-1.6.2.jar">
<sha256 value="5bb2381d819e6b79478ed086ef8fa1be595b16c253ae9774691a4575e4bbcc4a" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-desktop-1.6.2.module">
<sha256 value="3cfc02c617e773d2449b74634dd1c66c14d1ebac3aca4c1dee2a92b59f876d06" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-livedata" version="1.4.2">
<artifact name="runtime-livedata-1.4.2.aar">
<sha256 value="caa5f34b824f0df6fbf55cc2a55ffff371d365dbc8383a5faa9f2b57b63276cb" origin="Generated by Gradle"/>
@ -721,6 +944,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="eac600dec9b802388ccf1d22dc32e2496af99f02fc1ab47c2de3fbf2829226b9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-livedata" version="1.6.2">
<artifact name="runtime-livedata-1.6.2.aar">
<sha256 value="b58992a123e984aaf6e8e314360b94c57de66fbe85017bdcc3419f3e8eff4011" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-livedata-1.6.2.module">
<sha256 value="6f80e019de1986716a805403775e91f92bb6a0142211ff7cd0ba579135c9e110" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-rxjava3" version="1.4.2">
<artifact name="runtime-rxjava3-1.4.2.aar">
<sha256 value="685877e672f95cf7368ada6e3af07eee107529b6962bf2272b4b1c8d22da7200" origin="Generated by Gradle"/>
@ -745,6 +976,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="b1770a31b297982f21a01d1f2c243c7a1872cf4eccec7b314aa5d4a00fad9ed5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-rxjava3" version="1.6.2">
<artifact name="runtime-rxjava3-1.6.2.aar">
<sha256 value="2f326425ffe251bc6b60e764cd890e78cc0eae82012f87f9dfd2237c19d3599f" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-rxjava3-1.6.2.module">
<sha256 value="3108e86599972b2ecb8a24c263f2426dbb577e24c9f449713f171a1749128bc5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.0.1">
<artifact name="runtime-saveable-1.0.1.module">
<sha256 value="c0d6f142542d8d74f65481ef6526d2be265f01f812a112948fcde87a458f4fb6" origin="Generated by Gradle"/>
@ -774,6 +1013,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="473560f012448c5ffda939c3b43dd806428d0dfa77b53fe80018942f4555eb4f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable" version="1.6.2">
<artifact name="runtime-saveable-1.6.2.module">
<sha256 value="616baf3a8570bdb15febe1d9921656fb2bbabb25fa5e8b89fd349a80b415d617" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable-android" version="1.5.1">
<artifact name="runtime-saveable-android-1.5.1.module">
<sha256 value="173be33938c899999629cd3ab49502fbd8e807454381297bdc0da50bb23afc29" origin="Generated by Gradle"/>
@ -790,6 +1034,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5e834c12d7d226a1057e6e70b341cc27414afa2e9d0007660b9dffec6b7673ed" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable-android" version="1.6.2">
<artifact name="runtime-saveable-android-1.6.2.module">
<sha256 value="9f4d6d7da7fbed7b4213e9285d40e7fdb0e964ffbb6246240c2e2934b974598b" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-saveable-release.aar">
<sha256 value="2c32aa4864f373996bc035e8efce7b337591b4b42c4b6a0079519f00fe487fe6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.runtime" name="runtime-saveable-desktop" version="1.6.2">
<artifact name="runtime-saveable-desktop-1.6.2.jar">
<sha256 value="55064d4bd481d67a2840e52c594fc9cbbbf15c9a48f224199bbda005da705df4" origin="Generated by Gradle"/>
</artifact>
<artifact name="runtime-saveable-desktop-1.6.2.module">
<sha256 value="79c1cb4cd3998d08f145ed2424013c0c69ddf22112b8011e764664f5b20822e5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.0.1">
<artifact name="ui-1.0.1.aar">
<sha256 value="1943daa4a3412861b9a2bdc1a7c8c2ff05d9b8191c1d3e56ebb223d2eb4a8526" origin="Generated by Gradle"/>
@ -814,6 +1074,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="16047bacd8b5409b6b2c57a4761789dff9e6e93e6d35921846c364fcf3dfa1de" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui" version="1.6.2">
<artifact name="ui-1.6.2.module">
<sha256 value="af19eba05e4b1e56b1e61373c823a548782dc7bfadbc04fb43f13157214a5a28" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-android" version="1.5.1">
<artifact name="ui-android-1.5.1.module">
<sha256 value="a682e72c4f1a27387eaaf269679e62dc35e7398aae48fe099cf20b4f6cc1c04e" origin="Generated by Gradle"/>
@ -830,6 +1095,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="13f39a8f0bfaf621ab57f77ad022d482b5f2b14803a5623cc4fdca2ef905ca26" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-android" version="1.6.2">
<artifact name="ui-android-1.6.2.module">
<sha256 value="56694bbe7c44c15bdc2bdece7e6441becc7bab708877b4eca11c845b95471755" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-release.aar">
<sha256 value="9cd47d5634c08497920151d687942d11e4fe2a365255f089612d1cff724cb67a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-desktop" version="1.6.2">
<artifact name="ui-desktop-1.6.2.jar">
<sha256 value="45c06c1d10faad5fbe32fbd89e3d084281ccb8ae3d1318392410782e741ab948" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-desktop-1.6.2.module">
<sha256 value="0c544e4a8b68d8a0a1188525a5e6d4fcd702f152c51ad4bf9f51176075ff3425" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-geometry" version="1.0.1">
<artifact name="ui-geometry-1.0.1.aar">
<sha256 value="04dcea79de0af8347f4517bec20578d4240ae5ee40191020ca07d7a38e640033" origin="Generated by Gradle"/>
@ -854,6 +1135,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="1dd30ce8cca076140ed92ee748c1d35a4ac46b9da435406e353208a07267996d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-geometry" version="1.6.2">
<artifact name="ui-geometry-1.6.2.module">
<sha256 value="13e4ecec3172d7751115804d7121bd895357a33f8703e690b33677f062b13fbc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-geometry-android" version="1.5.1">
<artifact name="ui-geometry-android-1.5.1.module">
<sha256 value="1550bbf0c36e52e60ec9fbb896221115213ccd68296cacdfe62a4d3ea951b0f5" origin="Generated by Gradle"/>
@ -870,6 +1156,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2ecd05d0c033325304834a361d3e5f2c01395bfc5da2402641cfcee4981a01a9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-geometry-android" version="1.6.2">
<artifact name="ui-geometry-android-1.6.2.module">
<sha256 value="13edbe112aa62e29223d4583f4cfe5aef1a0b939de370c2a48cf65a594aae312" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-geometry-release.aar">
<sha256 value="74ed23d23fe42cc75a5619575c188ddbe552272d8308c58a864b6e75d8063bdb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-geometry-desktop" version="1.6.2">
<artifact name="ui-geometry-desktop-1.6.2.jar">
<sha256 value="c5ca7579c17710fac77ed93137411531298cca559c8514c9076bc0f2683da350" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-geometry-desktop-1.6.2.module">
<sha256 value="eba16975cac4719f4af6945c61d7cd84ad8072a667c49a0193d887bca3a838de" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-graphics" version="1.0.1">
<artifact name="ui-graphics-1.0.1.aar">
<sha256 value="6b7f5d0c944483ea390b3d6da8438e7a69e0cfe310bd16c3c5e78ac0015b1663" origin="Generated by Gradle"/>
@ -894,6 +1196,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="1f0880be97e83fd8e86ce0374937cd6947d3380a2df5ff4bb7cac47587dd601a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-graphics" version="1.6.2">
<artifact name="ui-graphics-1.6.2.module">
<sha256 value="a54e070d593f2f41c362e800c79ce5664ad795b7e0fb7e9c4c2f25ff234bfa12" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-graphics-android" version="1.5.1">
<artifact name="ui-graphics-android-1.5.1.module">
<sha256 value="6334183295fc3bf67230dad0b7fee097429ff61df500ee62fdaf22e82eaa11b7" origin="Generated by Gradle"/>
@ -910,6 +1217,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8f7811d34f178e2de66399e8b0fd747db3502a9b844a4e2d946a711e75655cbc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-graphics-android" version="1.6.2">
<artifact name="ui-graphics-android-1.6.2.module">
<sha256 value="bd3787bfb69eae61dafa93eb689e7eb68bb8154be7ca23bac37f36bfe9a22efc" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-graphics-release.aar">
<sha256 value="5a2b95861592e9f698d7ce9c6acb1eca008401802f75567efd609b628840970a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-graphics-desktop" version="1.6.2">
<artifact name="ui-graphics-desktop-1.6.2.jar">
<sha256 value="7d6f9ffc9804cdec9c270abe37c06dbb847fe42b8d316caa73b8135752b7dd66" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-graphics-desktop-1.6.2.module">
<sha256 value="0e6e228fb9ca8304e2b537b01d5a3968f1740064359d16805a155f7bdc9f8e62" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-test-manifest" version="1.5.1">
<artifact name="ui-test-manifest-1.5.1.aar">
<sha256 value="449a954b2b9908c0fb6faeada91e54749ce2cbb794e4bb0c03d5d27ae1115281" origin="Generated by Gradle"/>
@ -955,6 +1278,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="56968bd3be7d2bac45fd75252ac9b5f390601b44243d2a410926e6c0cd4f151f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-text" version="1.6.2">
<artifact name="ui-text-1.6.2.module">
<sha256 value="1e16728e46acba7422c668bb43c65103d11a3f03d338e4bab8148123ddf30c13" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-text-android" version="1.5.1">
<artifact name="ui-text-android-1.5.1.module">
<sha256 value="b6e932a918a9344d8f7e2f086a6b962d031d733678d6bbf6f6be4bd4a15f5259" origin="Generated by Gradle"/>
@ -971,6 +1299,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="1633dd54142169a5066580f9a580d2660c790fc1b68c2e533c36a9c0f8b10abb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-text-android" version="1.6.2">
<artifact name="ui-text-android-1.6.2.module">
<sha256 value="f292c95185b433eb4b5a15a561002ab078ce835aa341ddbc625c2c9ddef0df0c" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-text-release.aar">
<sha256 value="398cc1df7c583e48bae632b94573c509a134a8184af1ff4e7bc2e457e13fe513" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-text-desktop" version="1.6.2">
<artifact name="ui-text-desktop-1.6.2.jar">
<sha256 value="098ae9f6453b9a1b1fa5f77b8db2f0d0b372fe83ebcab5e5975f737946cc8ea1" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-text-desktop-1.6.2.module">
<sha256 value="f79fa0346d1a12aeacf9c854330b92c122b95fc28b59d1f9a69db1dd59801d11" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling" version="1.5.1">
<artifact name="ui-tooling-1.5.1.module">
<sha256 value="c217d8b53be36c3562f01bd2474c0244d1924ba6130f6bc5abf42ab277309087" origin="Generated by Gradle"/>
@ -987,6 +1331,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5180afdae77bb0f37b309c31a457641919ebb84012dde2abe93951fe070de3d7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling" version="1.6.2">
<artifact name="ui-tooling-1.6.2.module">
<sha256 value="0af4130b88c1acf0cd7a4bab8e616d02aa8c786caa44ad3fdf676a86f1b27654" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-android" version="1.5.1">
<artifact name="ui-tooling-android-1.5.1.module">
<sha256 value="f6559c93178b465f2fb8250f1cdf9dbf5f4845957f87b72a84a62d31b65f1ab3" origin="Generated by Gradle"/>
@ -1003,6 +1352,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="d7459ec30b27d1bd3e922fc69db1e27206a0721d825df44ffc7ef15842124264" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-android" version="1.6.2">
<artifact name="ui-tooling-android-1.6.2.module">
<sha256 value="6741e075c9ad6b02439bc25b362055d9859da51c38fcdfacb0e5def5435a3862" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-release.aar">
<sha256 value="dfef530da1a4f809fb58471e2d0f39801ee2f5426a992adf164e687ffc43de1e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-data" version="1.5.1">
<artifact name="ui-tooling-data-1.5.1.module">
<sha256 value="7a6133f6aed027d45a8fd51d8f90c971b0f97f5893a5d720d994a903dab86d82" origin="Generated by Gradle"/>
@ -1019,6 +1376,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="d13169117f6de9a39383c21087cc3a2a2c68f83b5d3d75f5e35bc91996662d66" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-data" version="1.6.2">
<artifact name="ui-tooling-data-1.6.2.module">
<sha256 value="e28c62dc2ee0d24f80246df240edcafd607bbac71af5953bb0a814692a1e00c5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-data-android" version="1.5.1">
<artifact name="ui-tooling-data-android-1.5.1.module">
<sha256 value="41a1a7e6603302598e1c28535ce7fdf7d66218057f4d8f1ddadd4b7a1bd7e444" origin="Generated by Gradle"/>
@ -1035,6 +1397,30 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="d8185c38707f10d094458ab5b8026de93916d925876b32ac53278b1113f66bf1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-data-android" version="1.6.2">
<artifact name="ui-tooling-data-android-1.6.2.module">
<sha256 value="521e06044f639b015125383d23121caa6adfc37d5607a00b848cd8b4553e9829" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-data-release.aar">
<sha256 value="6a04bac6a0636f3838ac30abf6bd509f4318c934e5ff796830a0c46b2d02e8dd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-data-desktop" version="1.6.2">
<artifact name="ui-tooling-data-desktop-1.6.2.jar">
<sha256 value="e217a177e64d450f68c43964d221afe92bee3b096bb02e9a9644bf23c210aae7" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-data-desktop-1.6.2.module">
<sha256 value="f9b6072ea7378c710e7f9bd488c5b063774262df7cbc1c1d8730bead15303c22" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-desktop" version="1.6.2">
<artifact name="ui-tooling-desktop-1.6.2.jar">
<sha256 value="b1fafbd90b0a84b22082e8cebd7761d52b1ce8db6c6cf28fcc76204ee1cfb909" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-desktop-1.6.2.module">
<sha256 value="bd95a0db3fbb9a66b91793dfafebc83f6f976a3d970905a15ddde9783c2fd784" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-preview" version="1.5.1">
<artifact name="ui-tooling-preview-1.5.1.module">
<sha256 value="7c0fd4f5a53604092325e58eb735002d16f94e6c646ba26b9e5e7496f444c7b9" origin="Generated by Gradle"/>
@ -1051,6 +1437,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="473560f012448c5ffda939c3b43dd806428d0dfa77b53fe80018942f4555eb4f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-preview" version="1.6.2">
<artifact name="ui-tooling-preview-1.6.2.module">
<sha256 value="27a950500f7ad19d930c2d4efb1ffd2768f9e1febc4b0ba6cb71900aefabea2c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-preview-android" version="1.5.1">
<artifact name="ui-tooling-preview-android-1.5.1.module">
<sha256 value="f5c9fc4eb46a414f55f1e6ba39ca0af84a6399b7ac4f8ff77efcf52cbc2da38e" origin="Generated by Gradle"/>
@ -1067,6 +1458,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="1263288480372dbfb54d63234e9cabfa7f94b13a5ba108d06c4aa1976dbbaade" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-preview-android" version="1.6.2">
<artifact name="ui-tooling-preview-android-1.6.2.module">
<sha256 value="bb08b9adb59b2f4bd8beea04778500d5ea6408c5166f8ff70a81ed51222ad1a7" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-preview-release.aar">
<sha256 value="ee041736f6830ac77b1ded08071d679b45306325f6dd4c5426bbea31fddca3c6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-tooling-preview-desktop" version="1.6.2">
<artifact name="ui-tooling-preview-desktop-1.6.2.jar">
<sha256 value="1b531c5ec345d60ff880895988e065c39b4d42c3e2cf57f811c2daa7fb2c7322" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-tooling-preview-desktop-1.6.2.module">
<sha256 value="e9d2f5a1edaed9a048d0a7c83ed34c7c3b44c700b3fe1bfa5e64fe7df3cf2617" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-unit" version="1.0.1">
<artifact name="ui-unit-1.0.1.aar">
<sha256 value="75b13e2a15da8a31acf75d81c4277ff2820b1bf58291b4688ac55200cdc18072" origin="Generated by Gradle"/>
@ -1091,6 +1498,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="a122255bc8f3d7fe1ad1aa4d971508ae552a0c60d05b9c82400f05e579984df1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-unit" version="1.6.2">
<artifact name="ui-unit-1.6.2.module">
<sha256 value="cde1d8e6671e4f1a14f8c737a63dbdd91fe0232e240f145b3c29c297bdf76dd9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-unit-android" version="1.5.1">
<artifact name="ui-unit-android-1.5.1.module">
<sha256 value="59b1f7938cd9ef40fa13550be3798e6466849460386d2b00a9489442690e544b" origin="Generated by Gradle"/>
@ -1107,6 +1519,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="ac753ab129b70bab79cfea691ed6bc8ec20fde278db7269b94f50227a712302e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-unit-android" version="1.6.2">
<artifact name="ui-unit-android-1.6.2.module">
<sha256 value="47bb7662d40774f8aa095b3e73021e1be126a8b9b458c028f29e0c710012a9ec" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-unit-release.aar">
<sha256 value="4294337fae5c9eb06b75439093f4d39a3c4a4e3f708c15ee2535790a070d5a64" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-unit-desktop" version="1.6.2">
<artifact name="ui-unit-desktop-1.6.2.jar">
<sha256 value="eed875aeb6593f2c2548184957f612a3c971c61f4f26ed23893e74dffe4cccc0" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-unit-desktop-1.6.2.module">
<sha256 value="376c2a185177a53cd28c9f90f1feeb16e8315bb695ded62502166ad5cb6dfe6c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-util" version="1.0.0">
<artifact name="ui-util-1.0.0.module">
<sha256 value="a09871728e5a9d050d2fdcb99a875ef2120dc6deea808f5a6d443dd887e081ca" origin="Generated by Gradle"/>
@ -1122,6 +1550,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="18b8add303065a8c078fc908584a1a9fe9887df49025e15eb64ee6251d306e4e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-util" version="1.6.2">
<artifact name="ui-util-1.6.2.module">
<sha256 value="d9c2cb14d2882994f4451da34c1efe81568278ffb5bd9dd37ec7c75f8a2be934" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-util-android" version="1.5.1">
<artifact name="ui-util-android-1.5.1.module">
<sha256 value="d6574ccd75f47ac2b2733095ab06c683ec23eb0cbe2e842c9a9bc3a62a825a6c" origin="Generated by Gradle"/>
@ -1138,6 +1571,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="02cd59abbccf95fef2825ba4c0871255380c6f5ba3eaea85b1892de24b4e6b50" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-util-android" version="1.6.2">
<artifact name="ui-util-android-1.6.2.module">
<sha256 value="86e955e15e2ac19f886bca2be157f04bdf60b716211f6a376823e02a6a0f7944" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-util-release.aar">
<sha256 value="05eb30cd374cdf91a69b2a1ab5a1ee41cae9f2576918adc5e0e20f6c42f4142e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.compose.ui" name="ui-util-desktop" version="1.6.2">
<artifact name="ui-util-desktop-1.6.2.jar">
<sha256 value="0cb9f941d92bb60e2c8deaac1ec8312d9484dce01eacec33003675525ae5915c" origin="Generated by Gradle"/>
</artifact>
<artifact name="ui-util-desktop-1.6.2.module">
<sha256 value="0c29231f79c66100409d3202ef8fe1e942feda555ae2be8ff91710491b14c1ca" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.concurrent" name="concurrent-futures" version="1.0.0">
<artifact name="concurrent-futures-1.0.0.jar">
<sha256 value="5595a40e278a7b39fa78a09490e3d7f3faa95c7b01447148bd38b5ade0605c35" origin="Generated by Gradle"/>
@ -1357,6 +1806,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="9d1996cca03777baa1f27cd15531db983a633dae37b90f85bd53501acb56699d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.emoji2" name="emoji2" version="1.3.0">
<artifact name="emoji2-1.3.0.aar">
<sha256 value="2bf23818b23a996ddaa1b5fd5bb32129daff6bbb2dce15166e2fccdd2010b1a5" origin="Generated by Gradle"/>
</artifact>
<artifact name="emoji2-1.3.0.module">
<sha256 value="ddc851edba65fd15a7a1bc3ad18648e2f732dd5ad8ef3602224bce0647f5b4d1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.emoji2" name="emoji2" version="1.4.0">
<artifact name="emoji2-1.4.0.aar">
<sha256 value="433febd3434a45667176c76a64f3f205ca6335a6b544c5b5d57f25a38a375242" origin="Generated by Gradle"/>
@ -1373,6 +1830,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="a3a9db581abf1787b01ff15c40f059530e8e64f3937caa2d788f42eb39893268" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.emoji2" name="emoji2-views-helper" version="1.3.0">
<artifact name="emoji2-views-helper-1.3.0.aar">
<sha256 value="9a1351295a4f739df0efe8344adaa9afb34856c3af584d4a9afbec105a45b90b" origin="Generated by Gradle"/>
</artifact>
<artifact name="emoji2-views-helper-1.3.0.module">
<sha256 value="09974bb5ef9780de9d56715d71171a35c78fb2e17fd865773b0c83a3acac039c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.emoji2" name="emoji2-views-helper" version="1.4.0">
<artifact name="emoji2-views-helper-1.4.0.aar">
<sha256 value="ed5d3ed772a5fbf0d570f7526f585cd61a180e60f9372584c328a68e2cff3375" origin="Generated by Gradle"/>
@ -2464,6 +2929,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="48f6b212e8f566a50b7188ce80a2648777c1624350975335cda139be0f34f720" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.airbnb.android" name="lottie" version="6.4.0">
<artifact name="lottie-6.4.0.aar">
<sha256 value="d6cf3be2c56fa250c96a86eb0baf8a7dfc3cc92b7e728a74f8851f9ad9fec2ba" origin="Generated by Gradle"/>
</artifact>
<artifact name="lottie-6.4.0.module">
<sha256 value="5c1d2787b267c5a023b13d252f9a9ab1b6aa3b86f083ae7ce2ccfec13a10290d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.airbnb.android" name="lottie-compose" version="6.4.0">
<artifact name="lottie-compose-6.4.0.aar">
<sha256 value="0db0405db586246750b3c2139dc840697623f0a0a3dff68041f41ca2ff408509" origin="Generated by Gradle"/>
</artifact>
<artifact name="lottie-compose-6.4.0.module">
<sha256 value="036310ba164264e7603dd050c25dc1b4b40e40672e2748573a7527da571a4844" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.almworks.sqlite4java" name="sqlite4java" version="1.0.392">
<artifact name="sqlite4java-1.0.392.jar">
<sha256 value="243a64470fda0e86a6fddeb0af4c7aa9426ce84e68cbfe18d75ee5da4b7e0b92" origin="Generated by Gradle"/>
@ -6948,6 +7429,9 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.22">
<artifact name="kotlin-stdlib-1.9.22-all.jar">
<sha256 value="cec38bc3302e72a8aaf9cde436b5a9071ee0331e2ad05e84d8bb897334d7e9d4" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-stdlib-1.9.22.jar">
<sha256 value="6abe146c27864138b874ccccfe5f534e3eb923c99a1b7b5d45494ee5694f3e0a" origin="Generated by Gradle"/>
</artifact>
@ -7326,6 +7810,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2eb0b5c00791576c33079a0750ef3ded335c180f60043d51a19c799f8b737bc5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-swing" version="1.8.1">
<artifact name="kotlinx-coroutines-swing-1.8.1.jar">
<sha256 value="0c809e79dc64ab5bc9b790389fe491cc19d3574345a431276966fbc09994a659" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlinx-coroutines-swing-1.8.1.module">
<sha256 value="2915188bf6dde89157cccb480bfa1249f010917a8e1fad340c9dbef6d7e0229b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.0.1">
<artifact name="kotlinx-serialization-core-1.0.1.module">
<sha256 value="9d0f1f6db25e394c89d58838eaebf3eabe360ef8ceb98aa3b9a4283532a54077" origin="Generated by Gradle"/>
@ -7391,6 +7883,19 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="c8fbfde4b5ee1e41a69175165e839991d1501665a7590e23162326501ac6122c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.skiko" name="skiko" version="0.7.7">
<artifact name="skiko-0.7.7.module">
<sha256 value="96b0bc4f70cc6c9dcace55897019eac065c56ab334b615832b23b7babdbf2cbb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.skiko" name="skiko-awt" version="0.7.7">
<artifact name="skiko-awt-0.7.7.jar">
<sha256 value="93c02a8d773e38d8081d565f83dc14c7e9c9e1607ae4f8791811dbd636711ce3" origin="Generated by Gradle"/>
</artifact>
<artifact name="skiko-awt-0.7.7.module">
<sha256 value="aa18d148903a8057e0cefc0a2ade4df408ea757a19c467ea68b826edcfcd7d5c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jlleitschuh.gradle" name="ktlint-gradle" version="11.1.0">
<artifact name="ktlint-gradle-11.1.0.jar">
<sha256 value="f2114379e18faca0f9bd90bdf4936e6484d0b4a85a3a94bcd12d5c102a339281" origin="Generated by Gradle"/>

View file

@ -23,6 +23,7 @@ class QrScannerView @JvmOverloads constructor(
private var scannerView: ScannerView? = null
private val qrDataPublish: PublishSubject<String> = PublishSubject.create()
private var forceLegacy: Boolean = false
val qrData: Observable<String> = qrDataPublish
@ -37,12 +38,14 @@ class QrScannerView @JvmOverloads constructor(
addView(scannerView)
this.scannerView = (scannerView as ScannerView)
this.forceLegacy = forceLegacy
}
@JvmOverloads
fun start(lifecycleOwner: LifecycleOwner, forceLegacy: Boolean = false) {
if (scannerView != null) {
Log.w(TAG, "Attempt to start scanning that has already started")
scannerView?.resume()
return
}
@ -61,6 +64,14 @@ class QrScannerView @JvmOverloads constructor(
scannerView?.toggleCamera()
}
// Biometrics require use of camera so we disable when needed
fun destroy() {
scannerView?.destroy()
if (!forceLegacy) {
scannerView = null
}
}
companion object {
private val TAG = Log.tag(QrScannerView::class.java)
}

View file

@ -8,4 +8,6 @@ import androidx.lifecycle.LifecycleOwner
interface ScannerView {
fun start(lifecycleOwner: LifecycleOwner)
fun toggleCamera()
fun resume()
fun destroy()
}

View file

@ -59,4 +59,16 @@ internal class ScannerView19 constructor(
lifecycleObserver.onResume(it)
}
}
override fun resume() {
lifecycleOwner?.let {
lifecycleObserver.onResume(it)
}
}
override fun destroy() {
lifecycleOwner?.let {
lifecycleObserver.onPause(it)
}
}
}

View file

@ -36,6 +36,7 @@ internal class ScannerView21 constructor(
private val lifecycleObserver: DefaultLifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
cameraProvider?.unbindAll()
cameraProvider = null
camera = null
analyzerExecutor.shutdown()
@ -78,6 +79,14 @@ internal class ScannerView21 constructor(
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
}
override fun resume() = Unit
override fun destroy() {
lifecyleOwner?.let {
lifecycleObserver.onDestroy(it)
}
}
private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) {
if (cameraProvider == null) {
Log.w(TAG, "Camera provider is null")