Fix QR icon background and changes for code review

This commit is contained in:
Andrew 2024-06-07 15:28:12 +09:30
parent 79c35b0e3b
commit f66fbef0ad
10 changed files with 88 additions and 84 deletions

View File

@ -3,5 +3,5 @@ package org.thoughtcrime.securesms.conversation.newmessage
interface Callbacks { interface Callbacks {
fun onChange(value: String) {} fun onChange(value: String) {}
fun onContinue() {} fun onContinue() {}
fun onScan(value: String) {} fun onScanQrCode(value: String) {}
} }

View File

@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.showOpenUrlDialog import org.thoughtcrime.securesms.showOpenUrlDialog
import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.LoadingArcOr import org.thoughtcrime.securesms.ui.LoadingArcOr
import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
@ -134,7 +133,7 @@ private fun NewMessage(
HorizontalPager(pagerState) { HorizontalPager(pagerState) {
when (TITLES[it]) { when (TITLES[it]) {
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp) R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
R.string.qrScan -> MaybeScanQrCode(errors, onScan = callbacks::onScan) R.string.qrScan -> MaybeScanQrCode(errors, onScan = callbacks::onScanQrCode)
} }
} }
} }

View File

@ -45,7 +45,7 @@ class NewMessageViewModel @Inject constructor(
createPrivateChatIfPossible(state.value.newMessageIdOrOns) createPrivateChatIfPossible(state.value.newMessageIdOrOns)
} }
override fun onScan(value: String) { override fun onScanQrCode(value: String) {
if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) { if (PublicKeyValidation.isValid(value, isPrefixRequired = false) && PublicKeyValidation.hasValidPrefix(value)) {
onPublicKey(value) onPublicKey(value)
} else { } else {

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.ui.ItemButton
import org.thoughtcrime.securesms.ui.classicDarkColors import org.thoughtcrime.securesms.ui.classicDarkColors
import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.AppBar
import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.medium import org.thoughtcrime.securesms.ui.medium
import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.ui.small
import javax.inject.Inject import javax.inject.Inject
@ -70,7 +71,7 @@ class NewConversationHomeFragment : Fragment() {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text(text = stringResource(R.string.qrYoursDescription), color = classicDarkColors[5], style = MaterialTheme.typography.small) Text(text = stringResource(R.string.qrYoursDescription), color = classicDarkColors[5], style = MaterialTheme.typography.small)
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
QrImage(string = TextSecurePreferences.getLocalNumber(requireContext())!!, contentDescription = stringResource(R.string.AccessibilityId_qr_code)) QrImage(string = TextSecurePreferences.getLocalNumber(requireContext())!!, Modifier.contentDescription(R.string.AccessibilityId_qr_code))
} }
} }
} }

View File

@ -103,7 +103,6 @@ import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.components.OutlineButton import org.thoughtcrime.securesms.ui.components.OutlineButton
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.ui.small
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities

View File

@ -4,19 +4,12 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
@ -26,7 +19,6 @@ import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class LinkDeviceEvent(val mnemonic: ByteArray) class LinkDeviceEvent(val mnemonic: ByteArray)
@ -59,7 +51,7 @@ class LinkDeviceViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
runDecodeCatching(string) runDecodeCatching(string)
.onSuccess(::onSuccess) .onSuccess(::onSuccess)
.onFailure(::onScanFailure) .onFailure(::onQrCodeScanFailure)
} }
} }
@ -82,7 +74,7 @@ class LinkDeviceViewModel @Inject constructor(
} }
} }
private fun onScanFailure(error: Throwable) { private fun onQrCodeScanFailure(error: Throwable) {
viewModelScope.launch { qrErrors.send(error) } viewModelScope.launch { qrErrors.send(error) }
} }

View File

@ -163,8 +163,8 @@ fun RecoveryPasswordCell(seed: String, copySeed:() -> Unit = {}) {
) { ) {
QrImage( QrImage(
seed, seed,
modifier = Modifier.padding(vertical = 24.dp), modifier = Modifier.padding(vertical = 24.dp)
contentDescription = stringResource(R.string.AccessibilityId_qr_code), .contentDescription(R.string.AccessibilityId_qr_code),
icon = R.drawable.session_shield icon = R.drawable.session_shield
) )
} }

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.preferences
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -28,6 +29,7 @@ import org.thoughtcrime.securesms.database.threadDatabase
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.setComposeContent import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.ui.small
import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.start
@ -43,7 +45,11 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title) supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
setComposeContent { setComposeContent {
Tabs(TextSecurePreferences.getLocalNumber(this)!!, errors.receiveAsFlow(), onScan = ::onScan) Tabs(
TextSecurePreferences.getLocalNumber(this)!!,
errors.receiveAsFlow(),
onScan = ::onScan
)
} }
} }
@ -86,13 +92,15 @@ private fun Tabs(sessionId: String, errors: Flow<String>, onScan: (String) -> Un
fun QrPage(string: String) { fun QrPage(string: String) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colors.surface)
.padding(horizontal = 32.dp) .padding(horizontal = 32.dp)
.fillMaxSize() .fillMaxSize()
) { ) {
QrImage( QrImage(
string = string, string = string,
contentDescription = stringResource(R.string.AccessibilityId_qr_code), modifier = Modifier
modifier = Modifier.padding(top = 32.dp, bottom = 12.dp), .padding(top = 32.dp, bottom = 12.dp)
.contentDescription(R.string.AccessibilityId_qr_code),
icon = R.drawable.session icon = R.drawable.session
) )

View File

@ -4,9 +4,11 @@ import android.graphics.Bitmap
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
@ -20,8 +22,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -34,7 +39,6 @@ import org.thoughtcrime.securesms.util.QRCodeUtilities
@Composable @Composable
fun QrImage( fun QrImage(
string: String, string: String,
contentDescription: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: Int = R.drawable.session_shield icon: Int = R.drawable.session_shield
) { ) {
@ -45,60 +49,63 @@ fun QrImage(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(string) { LaunchedEffect(string) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
val c = 100 bitmap = (300..500 step 100).firstNotNullOf {
val w = c * 2 runCatching { QRCodeUtilities.encode(string, it) }.getOrNull()
bitmap = QRCodeUtilities.encode(string, w).also {
val hw = 30
for (y in c - hw until c + hw) {
for (x in c - hw until c + hw) {
it.setPixel(x, y, 0x00000000)
}
}
} }
} }
} }
@Composable
fun content(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
AnimatedVisibility(
visible = bitmap != null,
enter = fadeIn(),
) {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(LocalOnLightCell.current)
)
}
}
Icon(
painter = painterResource(id = icon),
contentDescription = "",
tint = LocalOnLightCell.current,
modifier = Modifier
.align(Alignment.Center)
.size(60.dp)
)
}
}
if (MaterialTheme.colors.isLight) { if (MaterialTheme.colors.isLight) {
content(modifier) Content(bitmap, icon, modifier = modifier, backgroundColor = MaterialTheme.colors.surface)
} else { } else {
Card( Card(
backgroundColor = LocalLightCell.current, backgroundColor = LocalLightCell.current,
elevation = 0.dp, elevation = 0.dp,
modifier = modifier modifier = modifier
) { content() } ) { Content(bitmap, icon, modifier = Modifier.padding(16.dp), backgroundColor = LocalLightCell.current) }
} }
} }
@Composable
private fun Content(
bitmap: Bitmap?,
icon: Int,
modifier: Modifier = Modifier,
qrColor: Color = LocalOnLightCell.current,
backgroundColor: Color,
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
AnimatedVisibility(
visible = bitmap != null,
enter = fadeIn(),
) {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
colorFilter = ColorFilter.tint(qrColor),
// Use FilterQuality.None to keep QR edges sharp
filterQuality = FilterQuality.None
)
}
}
Icon(
painter = painterResource(id = icon),
contentDescription = "",
tint = LocalOnLightCell.current,
modifier = Modifier
.align(Alignment.Center)
.size(60.dp)
.background(color = backgroundColor)
)
}
}

View File

@ -4,8 +4,8 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
object QRCodeUtilities { object QRCodeUtilities {
@ -16,25 +16,23 @@ object QRCodeUtilities {
hasTransparentBackground: Boolean = true, hasTransparentBackground: Boolean = true,
dark: Int = Color.BLACK, dark: Int = Color.BLACK,
light: Int = Color.WHITE, light: Int = Color.WHITE,
): Bitmap { ): Bitmap? = runCatching {
try { val hints = hashMapOf(
val hints = hashMapOf( EncodeHintType.MARGIN to 1 ) EncodeHintType.MARGIN to 0,
val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints) EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H
val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) )
val color = if (isInverted) light else dark val color = if (isInverted) light else dark
val background = if (isInverted) dark else light val background = if (isInverted) dark else light
val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints)
Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888).apply {
for (y in 0 until result.height) { for (y in 0 until result.height) {
for (x in 0 until result.width) { for (x in 0 until result.width) {
if (result.get(x, y)) { when {
bitmap.setPixel(x, y, color) result.get(x, y) -> setPixel(x, y, color)
} else if (!hasTransparentBackground) { !hasTransparentBackground -> setPixel(x, y, background)
bitmap.setPixel(x, y, background)
} }
} }
} }
return bitmap
} catch (e: WriterException) {
return Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888)
} }
} }.getOrNull()
} }