Add ability to scan linked device qr code from gallery.

This commit is contained in:
Michelle Tang
2024-06-06 10:24:42 -07:00
committed by Alex Hart
parent 644b93e5a3
commit d9c42a4135
8 changed files with 67 additions and 10 deletions

View File

@@ -1105,7 +1105,7 @@
android:theme="@style/Theme.Signal.WallpaperCropper" android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/> android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity" <activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar" android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/> android:exported="false"/>

View File

@@ -19,9 +19,9 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment
/** /**
* Select username qr code from gallery instead of using camera. * Select qr code from gallery instead of using camera. Used in usernames and when linking devices
*/ */
class UsernameQrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragment.Callbacks { class QrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragment.Callbacks {
override fun attachBaseContext(newBase: Context) { override fun attachBaseContext(newBase: Context) {
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
@@ -50,7 +50,7 @@ class UsernameQrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragme
class Contract : ActivityResultContract<Unit, Uri?>() { class Contract : ActivityResultContract<Unit, Uri?>() {
override fun createIntent(context: Context, input: Unit): Intent { override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, UsernameQrImageSelectionActivity::class.java) return Intent(context, QrImageSelectionActivity::class.java)
} }
override fun parseResult(resultCode: Int, intent: Intent?): Uri? { override fun parseResult(resultCode: Int, intent: Intent?): Uri? {

View File

@@ -95,7 +95,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
galleryLauncher = registerForActivityResult(UsernameQrImageSelectionActivity.Contract()) { uri -> galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
if (uri != null) { if (uri != null) {
viewModel.scanImage(requireContext(), uri) viewModel.scanImage(requireContext(), uri)
} }

View File

@@ -42,6 +42,7 @@ import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableExtraCompat import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameQrScannerActivity.Contract
import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
@@ -70,7 +71,7 @@ class UsernameQrScannerActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
disposables.bindTo(this) disposables.bindTo(this)
val galleryLauncher = registerForActivityResult(UsernameQrImageSelectionActivity.Contract()) { uri -> val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
if (uri != null) { if (uri != null) {
viewModel.onQrImageSelected(this, uri) viewModel.onQrImageSelected(this, uri)
} }

View File

@@ -34,6 +34,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BiometricDeviceAuthentication import org.thoughtcrime.securesms.BiometricDeviceAuthentication
import org.thoughtcrime.securesms.BiometricDeviceLockContract import org.thoughtcrime.securesms.BiometricDeviceLockContract
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.QrImageSelectionActivity
import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -50,6 +51,7 @@ class AddLinkDeviceFragment : ComposeFragment() {
private val viewModel: LinkDeviceViewModel by activityViewModels() private val viewModel: LinkDeviceViewModel by activityViewModels()
private lateinit var biometricAuth: BiometricDeviceAuthentication private lateinit var biometricAuth: BiometricDeviceAuthentication
private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher<String> private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher<String>
private lateinit var galleryLauncher: ActivityResultLauncher<Unit>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -72,6 +74,12 @@ class AddLinkDeviceFragment : ComposeFragment() {
BiometricPrompt(requireActivity(), BiometricAuthenticationListener()), BiometricPrompt(requireActivity(), BiometricAuthenticationListener()),
promptInfo promptInfo
) )
galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
if (uri != null) {
viewModel.scanImage(requireContext(), uri)
}
}
} }
override fun onPause() { override fun onPause() {
@@ -113,7 +121,8 @@ class AddLinkDeviceFragment : ComposeFragment() {
viewModel.onLinkDeviceResult(true) viewModel.onLinkDeviceResult(true)
navController.popBackStack() navController.popBackStack()
}, },
onLinkDeviceFailure = { viewModel.onLinkDeviceResult(false) } onLinkDeviceFailure = { viewModel.onLinkDeviceResult(false) },
onGalleryOpened = { galleryLauncher.launch(Unit) }
) )
} }
@@ -161,7 +170,8 @@ private fun MainScreen(
onQrCodeDismissed: () -> Unit = {}, onQrCodeDismissed: () -> Unit = {},
onQrCodeRetry: () -> Unit = {}, onQrCodeRetry: () -> Unit = {},
onLinkDeviceSuccess: () -> Unit = {}, onLinkDeviceSuccess: () -> Unit = {},
onLinkDeviceFailure: () -> Unit = {} onLinkDeviceFailure: () -> Unit = {},
onGalleryOpened: () -> Unit = {}
) { ) {
Scaffolds.Settings( Scaffolds.Settings(
title = "", title = "",
@@ -188,6 +198,7 @@ private fun MainScreen(
linkDeviceResult = state.linkDeviceResult, linkDeviceResult = state.linkDeviceResult,
onLinkDeviceSuccess = onLinkDeviceSuccess, onLinkDeviceSuccess = onLinkDeviceSuccess,
onLinkDeviceFailure = onLinkDeviceFailure, onLinkDeviceFailure = onLinkDeviceFailure,
onGalleryOpened = onGalleryOpened,
modifier = Modifier.padding(contentPadding) modifier = Modifier.padding(contentPadding)
) )
} }

View File

@@ -37,6 +37,7 @@ fun LinkDeviceQrScanScreen(
linkDeviceResult: LinkDeviceRepository.LinkDeviceResult, linkDeviceResult: LinkDeviceRepository.LinkDeviceResult,
onLinkDeviceSuccess: () -> Unit, onLinkDeviceSuccess: () -> Unit,
onLinkDeviceFailure: () -> Unit, onLinkDeviceFailure: () -> Unit,
onGalleryOpened: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
@@ -106,7 +107,8 @@ fun LinkDeviceQrScanScreen(
}, },
hasPermission = hasPermission, hasPermission = hasPermission,
onRequestPermissions = onRequestPermissions, onRequestPermissions = onRequestPermissions,
qrString = stringResource(R.string.AddLinkDeviceFragment__scan_the_qr_code) qrString = stringResource(R.string.AddLinkDeviceFragment__scan_the_qr_code),
onGalleryOpened = onGalleryOpened
) )
} }
} }

View File

@@ -4,11 +4,15 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.signal.core.util.toOptional
import org.signal.qr.QrProcessor
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
@@ -168,4 +172,24 @@ class LinkDeviceViewModel : ViewModel() {
) )
} }
} }
fun scanImage(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
val loadBitmap = Glide.with(context)
.asBitmap()
.format(DecodeFormat.PREFER_ARGB_8888)
.load(uri)
.submit()
val result = QrProcessor().getScannedData(loadBitmap.get()).toOptional()
if (result.isPresent) {
onQrCodeScanned(result.get())
} else {
_state.value = _state.value.copy(
qrCodeInvalid = true,
showFrontCamera = null
)
}
}
}
} }

View File

@@ -1,13 +1,16 @@
package org.thoughtcrime.securesms.qr package org.thoughtcrime.securesms.qr
import android.content.Context import android.content.Context
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -17,16 +20,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.NoOpUpdate import androidx.compose.ui.viewinterop.NoOpUpdate
import org.signal.core.ui.Buttons import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.signal.qr.QrScannerView import org.signal.qr.QrScannerView
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
@@ -40,7 +46,8 @@ object QrScanScreens {
update: (QrScannerView) -> Unit = NoOpUpdate, update: (QrScannerView) -> Unit = NoOpUpdate,
hasPermission: Boolean, hasPermission: Boolean,
onRequestPermissions: () -> Unit = {}, onRequestPermissions: () -> Unit = {},
qrString: String qrString: String,
onGalleryOpened: () -> Unit = {}
) { ) {
val path = remember { Path() } val path = remember { Path() }
@@ -97,6 +104,18 @@ object QrScanScreens {
modifier = Modifier.padding(top = 24.dp).fillMaxWidth() modifier = Modifier.padding(top = 24.dp).fillMaxWidth()
) )
} }
FloatingActionButton(
shape = CircleShape,
containerColor = SignalTheme.colors.colorSurface1,
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 24.dp),
onClick = onGalleryOpened
) {
Image(
painter = painterResource(id = R.drawable.symbol_album_24),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
)
}
} }
} }
} }