mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Add ability to scan linked device qr code from gallery.
This commit is contained in:
@@ -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"/>
|
||||||
|
|||||||
@@ -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? {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user