mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Allow scanning QR code from 'Find by username' screen.
This commit is contained in:
committed by
Alex Hart
parent
c6df4af53a
commit
56b482a26f
@@ -1089,6 +1089,11 @@
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
|
||||
@@ -42,11 +42,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -67,6 +69,7 @@ import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Snackbars
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
@@ -75,6 +78,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.Use
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.UUID
|
||||
|
||||
@@ -130,14 +134,19 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
disposables = disposables.disposables,
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
galleryPermissionState = galleryPermissionState,
|
||||
onCodeTabSelected = { viewModel.onTabSelected(ActiveTab.Code) },
|
||||
onScanTabSelected = { viewModel.onTabSelected(ActiveTab.Scan) },
|
||||
onUsernameLinkResetResultHandled = { viewModel.onUsernameLinkResetResultHandled() },
|
||||
onShareBadge = { shareQrBadge(requireActivity(), viewModel.generateQrCodeImage(helpText)) },
|
||||
onQrCodeScanned = { data -> viewModel.onQrCodeScanned(data) },
|
||||
onQrResultHandled = { viewModel.onQrResultHandled() },
|
||||
onOpenGalleryClicked = { galleryLauncher.launch(Unit) },
|
||||
onOpenGalleryClicked = {
|
||||
if (galleryPermissionState.allPermissionsGranted) {
|
||||
galleryLauncher.launch(Unit)
|
||||
} else {
|
||||
galleryPermissionState.launchMultiplePermissionRequest()
|
||||
}
|
||||
},
|
||||
onLinkReset = { viewModel.onUsernameLinkReset() },
|
||||
onBackNavigationPressed = { requireActivity().onBackPressed() },
|
||||
linkCopiedEvent = linkCopiedEvent
|
||||
@@ -162,7 +171,6 @@ private fun MainScreen(
|
||||
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
|
||||
disposables: CompositeDisposable = CompositeDisposable(),
|
||||
cameraPermissionState: PermissionState = previewPermissionState(),
|
||||
galleryPermissionState: MultiplePermissionsState = previewMultiplePermissionState(),
|
||||
onCodeTabSelected: () -> Unit = {},
|
||||
onScanTabSelected: () -> Unit = {},
|
||||
onUsernameLinkResetResultHandled: () -> Unit = {},
|
||||
@@ -174,6 +182,8 @@ private fun MainScreen(
|
||||
onBackNavigationPressed: () -> Unit = {},
|
||||
linkCopiedEvent: UUID? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
val scope: CoroutineScope = rememberCoroutineScope()
|
||||
var showResetDialog: Boolean by remember { mutableStateOf(false) }
|
||||
@@ -231,12 +241,18 @@ private fun MainScreen(
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = disposables,
|
||||
galleryPermissionState = galleryPermissionState,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrCodeScanned,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
onRecipientFound = { recipient ->
|
||||
val taskStack = TaskStackBuilder
|
||||
.create(context)
|
||||
.addNextIntent(MainActivity.clearTop(context))
|
||||
|
||||
CommunicationActions.startConversation(context, recipient, null, taskStack)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -387,16 +403,6 @@ private fun previewPermissionState(): PermissionState {
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewMultiplePermissionState(): MultiplePermissionsState {
|
||||
return object : MultiplePermissionsState {
|
||||
override val allPermissionsGranted: Boolean = true
|
||||
override val permissions: List<PermissionState> = emptyList()
|
||||
override val revokedPermissions: List<PermissionState> = emptyList()
|
||||
override val shouldShowRationale: Boolean = false
|
||||
override fun launchMultiplePermissionRequest() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
private val previewLifecycleOwner: LifecycleOwner = object : LifecycleOwner {
|
||||
override val lifecycle: Lifecycle
|
||||
get() = throw UnsupportedOperationException("Only for tests")
|
||||
|
||||
@@ -27,8 +27,6 @@ import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@@ -37,8 +35,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.qr.QrProcessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||
@@ -48,7 +44,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.toLink
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
import java.util.Optional
|
||||
@@ -171,16 +166,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
indeterminateProgress = true
|
||||
)
|
||||
|
||||
disposable += UsernameRepository.fetchUsernameAndAciFromLink(url)
|
||||
.map { result ->
|
||||
when (result) {
|
||||
is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(Recipient.externalUsername(result.aci, result.username.toString()))
|
||||
is UsernameRepository.UsernameLinkConversionResult.Invalid -> QrScanResult.InvalidData
|
||||
is UsernameRepository.UsernameLinkConversionResult.NotFound -> QrScanResult.NotFound(result.username?.toString())
|
||||
is UsernameRepository.UsernameLinkConversionResult.NetworkError -> QrScanResult.NetworkError
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
disposable += UsernameQrScanRepository.lookupUsernameUrl(url)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
_state.value = _state.value.copy(
|
||||
@@ -201,25 +187,17 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun scanImage(context: Context, uri: Uri) {
|
||||
val loadBitmap = Glide.with(context)
|
||||
.asBitmap()
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.load(uri)
|
||||
.submit()
|
||||
_state.value = _state.value.copy(
|
||||
indeterminateProgress = true
|
||||
)
|
||||
|
||||
disposable += Single.fromFuture(loadBitmap)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { QrProcessor().getScannedData(it).toOptional() }
|
||||
disposable += UsernameQrScanRepository.scanImageUriForQrCode(context, uri)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
if (it.isPresent) {
|
||||
onQrCodeScanned(it.get())
|
||||
} else {
|
||||
_state.value = _state.value.copy(
|
||||
qrScanResult = QrScanResult.QrNotFound,
|
||||
indeterminateProgress = false
|
||||
)
|
||||
}
|
||||
.subscribeBy { result ->
|
||||
_state.value = _state.value.copy(
|
||||
qrScanResult = result,
|
||||
indeterminateProgress = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.toOptional
|
||||
import org.signal.qr.QrProcessor
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* A collection of functions to help with scanning QR codes for usernames.
|
||||
*/
|
||||
object UsernameQrScanRepository {
|
||||
|
||||
/**
|
||||
* Given a URL, will attempt to lookup the username, coercing it to a standard set of [QrScanResult]s.
|
||||
*/
|
||||
fun lookupUsernameUrl(url: String): Single<QrScanResult> {
|
||||
return UsernameRepository.fetchUsernameAndAciFromLink(url)
|
||||
.map { result ->
|
||||
when (result) {
|
||||
is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(Recipient.externalUsername(result.aci, result.username.toString()))
|
||||
is UsernameRepository.UsernameLinkConversionResult.Invalid -> QrScanResult.InvalidData
|
||||
is UsernameRepository.UsernameLinkConversionResult.NotFound -> QrScanResult.NotFound(result.username?.toString())
|
||||
is UsernameRepository.UsernameLinkConversionResult.NetworkError -> QrScanResult.NetworkError
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URI pointing to an image that may contain a username QR code, this will attempt to lookup the username, coercing it to a standard set of [QrScanResult]s.
|
||||
*/
|
||||
fun scanImageUriForQrCode(context: Context, uri: Uri): Single<QrScanResult> {
|
||||
val loadBitmap = Glide.with(context)
|
||||
.asBitmap()
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
.load(uri)
|
||||
.submit()
|
||||
|
||||
return Single.fromFuture(loadBitmap)
|
||||
.map { QrProcessor().getScannedData(it).toOptional() }
|
||||
.flatMap {
|
||||
if (it.isPresent) {
|
||||
lookupUsernameUrl(it.get())
|
||||
} else {
|
||||
Single.just(QrScanResult.QrNotFound)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
@@ -24,39 +24,33 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.qr.QrScannerView
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A screen that allows you to scan a QR code to start a chat.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun UsernameQrScanScreen(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
disposables: CompositeDisposable,
|
||||
galleryPermissionState: MultiplePermissionsState,
|
||||
qrScanResult: QrScanResult?,
|
||||
onQrCodeScanned: (String) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
onRecipientFound: (Recipient) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val path = remember { Path() }
|
||||
@@ -87,12 +81,7 @@ fun UsernameQrScanScreen(
|
||||
}
|
||||
|
||||
is QrScanResult.Success -> {
|
||||
val taskStack = TaskStackBuilder
|
||||
.create(LocalContext.current)
|
||||
.addNextIntent(MainActivity.clearTop(LocalContext.current))
|
||||
|
||||
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null, taskStack)
|
||||
onQrResultHandled()
|
||||
onRecipientFound(qrScanResult.recipient)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
@@ -134,13 +123,7 @@ fun UsernameQrScanScreen(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 24.dp),
|
||||
onClick = {
|
||||
if (galleryPermissionState.allPermissionsGranted) {
|
||||
onOpenGalleryClicked()
|
||||
} else {
|
||||
galleryPermissionState.launchMultiplePermissionRequest()
|
||||
}
|
||||
}
|
||||
onClick = onOpenGalleryClicked
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.symbol_album_24),
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.MultiplePermissionsState
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
/**
|
||||
* Prompts the user to scan a username QR code. Uses the activity result to communicate the recipient that was found, or null if no valid usernames were scanned.
|
||||
* See [Contract].
|
||||
*/
|
||||
class UsernameQrScannerActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val KEY_RECIPIENT_ID = "recipient_id"
|
||||
}
|
||||
|
||||
private val viewModel: UsernameQrScannerViewModel by viewModels()
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
disposables.bindTo(this)
|
||||
|
||||
val galleryLauncher = registerForActivityResult(UsernameQrImageSelectionActivity.Contract()) { uri ->
|
||||
if (uri != null) {
|
||||
viewModel.onQrImageSelected(this, uri)
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val galleryPermissionState: MultiplePermissionsState = rememberMultiplePermissionsState(permissions = PermissionCompat.forImages().toList()) { grants ->
|
||||
if (grants.values.all { it }) {
|
||||
galleryLauncher.launch(Unit)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.ChatWallpaperPreviewActivity__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
val state by viewModel.state
|
||||
|
||||
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
|
||||
Content(
|
||||
lifecycleOwner = this,
|
||||
diposables = disposables.disposables,
|
||||
state = state,
|
||||
galleryPermissionsState = galleryPermissionState,
|
||||
onQrScanned = { url -> viewModel.onQrScanned(url) },
|
||||
onQrResultHandled = {
|
||||
finish()
|
||||
},
|
||||
onOpenGalleryClicked = {
|
||||
if (galleryPermissionState.allPermissionsGranted) {
|
||||
galleryLauncher.launch(Unit)
|
||||
} else {
|
||||
galleryPermissionState.launchMultiplePermissionRequest()
|
||||
}
|
||||
},
|
||||
onRecipientFound = { recipient ->
|
||||
val intent = Intent().apply {
|
||||
putExtra(KEY_RECIPIENT_ID, recipient.id)
|
||||
}
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
},
|
||||
onBackNavigationPressed = {
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<Unit, RecipientId?>() {
|
||||
override fun createIntent(context: Context, input: Unit): Intent {
|
||||
return Intent(context, UsernameQrScannerActivity::class.java)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): RecipientId? {
|
||||
return intent?.getParcelableExtraCompat(KEY_RECIPIENT_ID, RecipientId::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Content(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
diposables: CompositeDisposable,
|
||||
state: UsernameQrScannerViewModel.ScannerState,
|
||||
galleryPermissionsState: MultiplePermissionsState,
|
||||
onQrScanned: (String) -> Unit,
|
||||
onQrResultHandled: () -> Unit,
|
||||
onOpenGalleryClicked: () -> Unit,
|
||||
onRecipientFound: (Recipient) -> Unit,
|
||||
onBackNavigationPressed: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBackNavigationPressed
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { contentPadding ->
|
||||
UsernameQrScanScreen(
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
disposables = diposables,
|
||||
qrScanResult = state.qrScanResult,
|
||||
onQrCodeScanned = onQrScanned,
|
||||
onQrResultHandled = onQrResultHandled,
|
||||
onOpenGalleryClicked = onOpenGalleryClicked,
|
||||
onRecipientFound = onRecipientFound,
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
)
|
||||
|
||||
if (state.indeterminateProgress) {
|
||||
Dialogs.IndeterminateProgressDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
|
||||
class UsernameQrScannerViewModel : ViewModel() {
|
||||
|
||||
private val _state = mutableStateOf(ScannerState(qrScanResult = null, indeterminateProgress = false))
|
||||
val state: State<ScannerState> = _state
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
fun onQrScanned(url: String) {
|
||||
_state.value = state.value.copy(indeterminateProgress = true)
|
||||
|
||||
disposables += UsernameQrScanRepository.lookupUsernameUrl(url)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result ->
|
||||
_state.value = _state.value.copy(
|
||||
qrScanResult = result,
|
||||
indeterminateProgress = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onQrImageSelected(context: Context, uri: Uri) {
|
||||
_state.value = state.value.copy(indeterminateProgress = true)
|
||||
|
||||
disposables += UsernameQrScanRepository.scanImageUriForQrCode(context, uri)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy { result ->
|
||||
_state.value = _state.value.copy(
|
||||
qrScanResult = result,
|
||||
indeterminateProgress = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
data class ScannerState(
|
||||
val qrScanResult: QrScanResult?,
|
||||
val indeterminateProgress: Boolean
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
@@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -77,6 +79,7 @@ import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameQrScannerActivity
|
||||
import org.thoughtcrime.securesms.invites.InviteActions
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -101,6 +104,13 @@ class FindByActivity : PassphraseRequiredActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
val qrScanLauncher: ActivityResultLauncher<Unit> = registerForActivityResult(UsernameQrScannerActivity.Contract()) { recipientId ->
|
||||
if (recipientId != null) {
|
||||
setResult(RESULT_OK, Intent().putExtra(RECIPIENT_ID, recipientId))
|
||||
finishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
|
||||
@@ -145,6 +155,9 @@ class FindByActivity : PassphraseRequiredActivity() {
|
||||
},
|
||||
onSelectCountryPrefixClick = {
|
||||
navController.navigate("select-country-prefix")
|
||||
},
|
||||
onQrCodeScanClicked = {
|
||||
qrScanLauncher.launch(Unit)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -273,7 +286,8 @@ private fun Content(
|
||||
state: FindByState,
|
||||
onUserEntryChanged: (String) -> Unit,
|
||||
onNextClick: () -> Unit,
|
||||
onSelectCountryPrefixClick: () -> Unit
|
||||
onSelectCountryPrefixClick: () -> Unit,
|
||||
onQrCodeScanClicked: () -> Unit
|
||||
) {
|
||||
val placeholderLabel = remember(state.mode) {
|
||||
if (state.mode == FindByMode.PHONE_NUMBER) R.string.FindByActivity__phone_number else R.string.FindByActivity__username
|
||||
@@ -364,6 +378,23 @@ private fun Content(
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Buttons.Small(onClick = onQrCodeScanClicked) {
|
||||
Icon(painter = painterResource(id = R.drawable.symbol_qrcode_24), contentDescription = stringResource(id = R.string.FindByActivity__qr_scan_button))
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.FindByActivity__qr_scan_button),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -541,8 +572,8 @@ private fun CountryPrefixRowItem(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Light Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "content - phone", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreviewPhoneNumber() {
|
||||
Previews.Preview {
|
||||
@@ -554,13 +585,14 @@ private fun ContentPreviewPhoneNumber() {
|
||||
),
|
||||
onUserEntryChanged = {},
|
||||
onNextClick = {},
|
||||
onSelectCountryPrefixClick = {}
|
||||
onSelectCountryPrefixClick = {},
|
||||
onQrCodeScanClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Light Theme", group = "content - username", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "content - username", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreviewUsername() {
|
||||
Previews.Preview {
|
||||
@@ -572,7 +604,8 @@ private fun ContentPreviewUsername() {
|
||||
),
|
||||
onUserEntryChanged = {},
|
||||
onNextClick = {},
|
||||
onSelectCountryPrefixClick = {}
|
||||
onSelectCountryPrefixClick = {},
|
||||
onQrCodeScanClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6621,6 +6621,8 @@
|
||||
<string name="FindByActivity__s_is_not_a_signal_user_would">%1$s is not a Signal user. Would you like to invite this number?</string>
|
||||
<!-- Dialog action to invite the phone number to Signal -->
|
||||
<string name="FindByActivity__invite">Invite</string>
|
||||
<!-- Button label for a button that will launch a camera to scan a username QR code -->
|
||||
<string name="FindByActivity__qr_scan_button">Scan QR code</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user