Add ability to scan username qr from gallery.

This commit is contained in:
Cody Henthorne 2024-02-28 12:44:17 -05:00 committed by Alex Hart
parent 6104ef62df
commit 86afa988a0
10 changed files with 255 additions and 21 deletions

View file

@ -1084,6 +1084,11 @@
android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"

View file

@ -13,4 +13,6 @@ sealed class QrScanResult {
object InvalidData : QrScanResult()
object NetworkError : QrScanResult()
object QrNotFound : QrScanResult()
}

View file

@ -8,6 +8,8 @@ import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
@ -52,9 +54,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
@ -69,6 +73,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeSt
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.permissions.PermissionCompat
import org.thoughtcrime.securesms.providers.BlobProvider
import java.io.ByteArrayOutputStream
import java.util.UUID
@ -79,6 +84,18 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
private val disposables: LifecycleDisposable = LifecycleDisposable()
private lateinit var galleryLauncher: ActivityResultLauncher<Unit>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
galleryLauncher = registerForActivityResult(UsernameQrImageSelectionActivity.Contract()) { uri ->
if (uri != null) {
viewModel.scanImage(requireContext(), uri)
}
}
}
override fun onStart() {
super.onStart()
setFragmentResultListener(UsernameLinkShareBottomSheet.REQUEST_KEY) { key, bundle ->
@ -99,18 +116,28 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
viewModel.onTabSelected(ActiveTab.Scan)
}
val galleryPermissionState: MultiplePermissionsState = rememberMultiplePermissionsState(permissions = PermissionCompat.forImages().toList()) { grants ->
if (grants.values.all { it }) {
galleryLauncher.launch(Unit)
} else {
Toast.makeText(requireContext(), R.string.ChatWallpaperPreviewActivity__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT).show()
}
}
MainScreen(
state = state,
navController = navController,
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) },
onLinkReset = { viewModel.onUsernameLinkReset() },
onBackNavigationPressed = { requireActivity().onBackPressed() },
linkCopiedEvent = linkCopiedEvent
@ -127,6 +154,7 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun MainScreen(
state: UsernameLinkSettingsState,
@ -134,12 +162,14 @@ private fun MainScreen(
lifecycleOwner: LifecycleOwner = previewLifecycleOwner,
disposables: CompositeDisposable = CompositeDisposable(),
cameraPermissionState: PermissionState = previewPermissionState(),
galleryPermissionState: MultiplePermissionsState = previewMultiplePermissionState(),
onCodeTabSelected: () -> Unit = {},
onScanTabSelected: () -> Unit = {},
onUsernameLinkResetResultHandled: () -> Unit = {},
onShareBadge: () -> Unit = {},
onQrCodeScanned: (String) -> Unit = {},
onQrResultHandled: () -> Unit = {},
onOpenGalleryClicked: () -> Unit = {},
onLinkReset: () -> Unit = {},
onBackNavigationPressed: () -> Unit = {},
linkCopiedEvent: UUID? = null
@ -201,9 +231,11 @@ private fun MainScreen(
UsernameQrScanScreen(
lifecycleOwner = lifecycleOwner,
disposables = disposables,
galleryPermissionState = galleryPermissionState,
qrScanResult = state.qrScanResult,
onQrCodeScanned = onQrCodeScanned,
onQrResultHandled = onQrResultHandled,
onOpenGalleryClicked = onOpenGalleryClicked,
modifier = Modifier.padding(contentPadding)
)
}
@ -355,6 +387,16 @@ 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")

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
@ -9,6 +10,7 @@ import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
@ -25,13 +27,18 @@ 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
import io.reactivex.rxjava3.kotlin.plusAssign
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
@ -193,6 +200,29 @@ class UsernameLinkSettingsViewModel : ViewModel() {
_linkCopiedEvent.value = UUID.randomUUID()
}
fun scanImage(context: Context, uri: Uri) {
val loadBitmap = Glide.with(context)
.asBitmap()
.format(DecodeFormat.PREFER_ARGB_8888)
.load(uri)
.submit()
disposable += Single.fromFuture(loadBitmap)
.subscribeOn(Schedulers.io())
.map { QrProcessor().getScannedData(it).toOptional() }
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
if (it.isPresent) {
onQrCodeScanned(it.get())
} else {
_state.value = _state.value.copy(
qrScanResult = QrScanResult.QrNotFound,
indeterminateProgress = false
)
}
}
}
private fun generateQrCodeData(url: Optional<String>): Single<Optional<QrCodeData>> {
return Single.fromCallable {
url.map { QrCodeData.forData(it, 64) }

View file

@ -0,0 +1,64 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment
/**
* Select username qr code from gallery instead of using camera.
*/
class UsernameQrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragment.Callbacks {
override fun attachBaseContext(newBase: Context) {
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
setContentView(R.layout.username_qr_image_selection_activity)
}
@SuppressLint("LogTagInlined")
override fun onMediaSelected(media: Media) {
setResult(RESULT_OK, Intent().setData(media.uri))
finish()
}
override fun onToolbarNavigationClicked() {
setResult(RESULT_CANCELED)
finish()
}
override fun isCameraEnabled() = false
override fun isMultiselectEnabled() = false
class Contract : ActivityResultContract<Unit, Uri?>() {
override fun createIntent(context: Context, input: Unit): Intent {
return Intent(context, UsernameQrImageSelectionActivity::class.java)
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (resultCode == RESULT_OK) {
intent?.data
} else {
null
}
}
}
}

View file

@ -1,11 +1,15 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -15,19 +19,26 @@ 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.ColorFilter
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
@ -36,36 +47,51 @@ 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,
modifier: Modifier = Modifier
) {
val path = remember { Path() }
when (qrScanResult) {
QrScanResult.InvalidData -> {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_invalid), onDismiss = onQrResultHandled)
QrScanResultDialog(message = stringResource(R.string.UsernameLinkSettings_qr_result_invalid), onDismiss = onQrResultHandled)
}
QrScanResult.NetworkError -> {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_network_error), onDismiss = onQrResultHandled)
QrScanResultDialog(message = stringResource(R.string.UsernameLinkSettings_qr_result_network_error), onDismiss = onQrResultHandled)
}
QrScanResult.QrNotFound -> {
QrScanResultDialog(
title = stringResource(R.string.UsernameLinkSettings_qr_code_not_found),
message = stringResource(R.string.UsernameLinkSettings_try_scanning_another_image_containing_a_signal_qr_code),
onDismiss = onQrResultHandled
)
}
is QrScanResult.NotFound -> {
if (qrScanResult.username != null) {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found, qrScanResult.username), onDismiss = onQrResultHandled)
QrScanResultDialog(message = stringResource(R.string.UsernameLinkSettings_qr_result_not_found, qrScanResult.username), onDismiss = onQrResultHandled)
} else {
QrScanResultDialog(stringResource(R.string.UsernameLinkSettings_qr_result_not_found_no_username), onDismiss = onQrResultHandled)
QrScanResultDialog(message = stringResource(R.string.UsernameLinkSettings_qr_result_not_found_no_username), onDismiss = onQrResultHandled)
}
}
is QrScanResult.Success -> {
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null)
val taskStack = TaskStackBuilder
.create(LocalContext.current)
.addNextIntent(MainActivity.clearTop(LocalContext.current))
CommunicationActions.startConversation(LocalContext.current, qrScanResult.recipient, null, taskStack)
onQrResultHandled()
}
@ -77,25 +103,52 @@ fun UsernameQrScanScreen(
.fillMaxWidth()
.fillMaxHeight()
) {
AndroidView(
factory = { context ->
val view = QrScannerView(context)
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
onQrCodeScanned(data)
}
view
},
update = { view ->
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXModelBlocklist.isBlocklisted())
},
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
.drawWithContent {
drawContent()
drawQrCrosshair(path)
) {
AndroidView(
factory = { context ->
val view = QrScannerView(context)
disposables += view.qrData.throttleFirst(3000, TimeUnit.MILLISECONDS).subscribe { data ->
onQrCodeScanned(data)
}
view
},
update = { view ->
view.start(lifecycleOwner = lifecycleOwner, forceLegacy = CameraXModelBlocklist.isBlocklisted())
},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.drawWithContent {
drawContent()
drawQrCrosshair(path)
}
)
FloatingActionButton(
shape = CircleShape,
containerColor = SignalTheme.colors.colorSurface1,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp),
onClick = {
if (galleryPermissionState.allPermissionsGranted) {
onOpenGalleryClicked()
} else {
galleryPermissionState.launchMultiplePermissionRequest()
}
}
)
) {
Image(
painter = painterResource(id = R.drawable.symbol_album_24),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
)
}
}
Row(
modifier = Modifier
@ -114,8 +167,9 @@ fun UsernameQrScanScreen(
}
@Composable
private fun QrScanResultDialog(message: String, onDismiss: () -> Unit) {
private fun QrScanResultDialog(title: String? = null, message: String, onDismiss: () -> Unit) {
Dialogs.SimpleMessageDialog(
title = title,
message = message,
dismiss = stringResource(id = android.R.string.ok),
onDismiss = onDismiss

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9 11.88c0-0.83 0.67-1.5 1.5-1.5s1.5 0.67 1.5 1.5c0 0.82-0.67 1.5-1.5 1.5S9 12.7 9 11.88Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M18.87 7.13c0-0.52 0-0.97-0.03-1.34-0.04-0.47-0.12-0.91-0.33-1.32-0.33-0.64-0.84-1.15-1.48-1.48-0.4-0.2-0.85-0.29-1.32-0.33-0.45-0.04-1-0.04-1.67-0.04H5.96c-0.67 0-1.22 0-1.67 0.04C3.82 2.7 3.38 2.78 2.97 3 2.33 3.32 1.82 3.83 1.49 4.47 1.3 4.87 1.2 5.32 1.16 5.79c-0.04 0.45-0.04 1-0.03 1.67v4.08c0 0.67 0 1.22 0.03 1.67 0.04 0.47 0.12 0.91 0.33 1.32 0.33 0.64 0.84 1.15 1.48 1.48 0.4 0.2 0.85 0.29 1.32 0.33 0.24 0.02 0.52 0.03 0.83 0.03v0.17c0 0.67 0 1.22 0.04 1.67 0.04 0.47 0.12 0.91 0.33 1.32 0.33 0.64 0.84 1.15 1.48 1.48 0.4 0.2 0.85 0.29 1.32 0.33 0.45 0.04 1 0.04 1.67 0.04h8.08c0.67 0 1.22 0 1.67-0.04 0.47-0.04 0.91-0.12 1.32-0.33 0.64-0.33 1.15-0.84 1.48-1.48 0.2-0.4 0.29-0.85 0.33-1.32 0.04-0.45 0.04-1 0.04-1.67v-4.58c0-0.67 0-1.22-0.04-1.67-0.04-0.47-0.12-0.91-0.33-1.32-0.33-0.64-0.84-1.15-1.48-1.48-0.4-0.2-0.85-0.29-1.32-0.33-0.24-0.02-0.52-0.03-0.84-0.03Zm-1.75 0H9.96c-0.67 0-1.22 0-1.67 0.03C7.82 7.2 7.38 7.28 6.97 7.5 6.33 7.82 5.82 8.33 5.49 8.97c-0.2 0.4-0.29 0.85-0.33 1.32-0.04 0.45-0.04 1-0.04 1.67v2.66c-0.27 0-0.5-0.01-0.7-0.03-0.35-0.03-0.53-0.08-0.66-0.14-0.3-0.16-0.55-0.4-0.7-0.71-0.07-0.13-0.12-0.3-0.15-0.67-0.03-0.37-0.04-0.86-0.04-1.57v-4c0-0.71 0-1.2 0.04-1.57 0.03-0.36 0.08-0.54 0.14-0.67 0.16-0.3 0.4-0.55 0.71-0.7 0.13-0.07 0.3-0.12 0.67-0.15C4.8 4.38 5.29 4.37 6 4.37h8c0.71 0 1.2 0 1.57 0.04 0.36 0.03 0.54 0.08 0.67 0.14 0.3 0.16 0.55 0.4 0.7 0.71 0.07 0.13 0.12 0.3 0.15 0.67 0.03 0.3 0.03 0.68 0.03 1.2Zm3.83 2.63c0.06 0.13 0.11 0.3 0.14 0.67 0.03 0.37 0.04 0.86 0.04 1.57v3.39l-1.95-1.95c-0.93-0.93-2.43-0.93-3.36 0L13 16.26l-0.82-0.82c-0.93-0.93-2.43-0.93-3.36 0l-1.94 1.94V16.5 12c0-0.71 0-1.2 0.03-1.57 0.03-0.36 0.08-0.54 0.14-0.67 0.16-0.3 0.4-0.55 0.71-0.7 0.13-0.07 0.3-0.12 0.67-0.15C8.8 8.88 9.29 8.87 10 8.87h8c0.71 0 1.2 0 1.57 0.04 0.36 0.03 0.54 0.08 0.67 0.14 0.3 0.16 0.55 0.4 0.7 0.71Zm-3 4.92l3.16 3.17-0.02 0.22c-0.03 0.36-0.08 0.54-0.14 0.67-0.16 0.3-0.4 0.55-0.71 0.7-0.13 0.07-0.3 0.12-0.67 0.15-0.37 0.03-0.86 0.04-1.57 0.04h-8c-0.71 0-1.2 0-1.57-0.04-0.36-0.03-0.54-0.08-0.67-0.14-0.1-0.05-0.2-0.12-0.28-0.19l2.58-2.58c0.24-0.24 0.64-0.24 0.88 0l1.44 1.44c0.34 0.34 0.9 0.34 1.24 0l3.44-3.44c0.24-0.24 0.64-0.24 0.88 0Z"/>
</vector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment"
tools:viewBindingIgnore="true" />

View file

@ -6526,6 +6526,10 @@
<string name="UsernameLinkSettings_reset_link_result_success">Your QR code and link have been reset and a new QR code and link has been created.</string>
<!-- Shown on the generated username qr code image to explain how to use it. -->
<string name="UsernameLinkSettings_scan_this_qr_code">Scan this QR code with your phone to chat with me on Signal.</string>
<!-- Dialog title shown when scanning an image from the gallery for a username QR code and there is no qr code in the image. -->
<string name="UsernameLinkSettings_qr_code_not_found">QR code not found</string>
<!-- Dialog message shown when scanning an image from the gallery for a username QR code and there is no qr code in the image. -->
<string name="UsernameLinkSettings_try_scanning_another_image_containing_a_signal_qr_code">Try scanning another image containing a Signal QR code.</string>
<!-- Explanatory text at the top of a bottom sheet describing how username links work -->
<string name="UsernameLinkShareBottomSheet_title">Anyone with this link can view your username and start a chat with you. Only share it with people you trust.</string>

View file

@ -1,5 +1,6 @@
package org.signal.qr
import android.graphics.Bitmap
import androidx.camera.core.ImageProxy
import com.google.zxing.BinaryBitmap
import com.google.zxing.ChecksumException
@ -8,10 +9,12 @@ import com.google.zxing.FormatException
import com.google.zxing.LuminanceSource
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader
import org.signal.core.util.logging.Log
import java.nio.IntBuffer
/**
* Wraps [QRCodeReader] for use from API19 or API21+.
@ -35,6 +38,16 @@ class QrProcessor {
return getScannedData(PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false))
}
fun getScannedData(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
val buffer = IntBuffer.allocate((bitmap.byteCount / 4) + 1)
bitmap.copyPixelsToBuffer(buffer)
return getScannedData(RGBLuminanceSource(bitmap.width, bitmap.height, buffer.array()))
}
private fun getScannedData(source: LuminanceSource): String? {
try {
if (source.width != previousWidth || source.height != previousHeight) {