Fix username QR code sharing.

This commit is contained in:
Greyson Parrelli 2023-11-07 11:43:36 -05:00
parent 7f2b6a874e
commit 423719e7bc
6 changed files with 147 additions and 41 deletions

View file

@ -53,12 +53,12 @@ fun QrCode(
}
}
private fun DrawScope.drawQr(
fun DrawScope.drawQr(
data: QrCodeData,
foregroundColor: Color,
backgroundColor: Color,
deadzonePercent: Float,
logo: ImageBitmap
logo: ImageBitmap?
) {
val deadzonePaddingPercent = 0.045f
@ -120,12 +120,14 @@ private fun DrawScope.drawQr(
// Logo
val logoWidthPx = (((deadzonePercent - deadzonePaddingPercent) * 0.6f) * size.width).toInt()
val logoOffsetPx = ((size.width - logoWidthPx) / 2).toInt()
drawImage(
image = logo,
dstOffset = IntOffset(logoOffsetPx, logoOffsetPx),
dstSize = IntSize(logoWidthPx, logoWidthPx),
colorFilter = ColorFilter.tint(foregroundColor)
)
if (logo != null) {
drawImage(
image = logo,
dstOffset = IntOffset(logoOffsetPx, logoOffsetPx),
dstSize = IntSize(logoWidthPx, logoWidthPx),
colorFilter = ColorFilter.tint(foregroundColor)
)
}
}
@Preview

View file

@ -24,17 +24,12 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -45,8 +40,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ScreenshotController
import org.thoughtcrime.securesms.compose.getScreenshotBounds
/**
* Renders a QR code and username as a badge.
@ -57,23 +50,16 @@ fun QrCodeBadge(
colorScheme: UsernameQrCodeColorScheme,
username: String,
modifier: Modifier = Modifier,
screenshotController: ScreenshotController? = null,
usernameCopyable: Boolean = false,
onClick: (() -> Unit) = {}
) {
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor, label = "border")
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor, label = "foreground")
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f, label = "elevation")
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White, label = "textColor")
var badgeBounds by remember {
mutableStateOf<Rect?>(null)
}
screenshotController?.bind(LocalView.current, badgeBounds)
val textColor by animateColorAsState(targetValue = colorScheme.textColor, label = "textColor")
Surface(
modifier = modifier
.onGloballyPositioned {
badgeBounds = it.getScreenshotBounds()
},
modifier = modifier,
color = borderColor,
shape = RoundedCornerShape(24.dp),
shadowElevation = elevation.dp
@ -99,8 +85,8 @@ fun QrCodeBadge(
data = data.data,
modifier = Modifier
.border(
width = if (colorScheme == UsernameQrCodeColorScheme.White) 2.dp else 0.dp,
color = Color(0xFFE9E9E9),
width = 2.dp,
color = colorScheme.outlineColor,
shape = RoundedCornerShape(size = 12.dp)
)
.padding(16.dp),

View file

@ -8,6 +8,8 @@ import androidx.compose.ui.graphics.Color
enum class UsernameQrCodeColorScheme(
val borderColor: Color,
val foregroundColor: Color,
val textColor: Color = Color.White,
val outlineColor: Color = Color.Transparent,
private val key: String
) {
Blue(
@ -18,6 +20,8 @@ enum class UsernameQrCodeColorScheme(
White(
borderColor = Color(0xFFFFFFFF),
foregroundColor = Color(0xFF000000),
textColor = Color.Black,
outlineColor = Color(0xFFE9E9E9),
key = "white"
),
Grey(

View file

@ -114,9 +114,8 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
modifier = Modifier.padding(contentPadding),
navController = navController,
onShareBadge = {
shareQrBadge(it)
shareQrBadge(viewModel.generateQrCodeImage())
},
screenshotController = screenshotController,
onResetClicked = { showResetDialog = true },
onLinkResultHandled = { viewModel.onUsernameLinkResetResultHandled() }
)
@ -278,7 +277,11 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
}
}
private fun shareQrBadge(badge: Bitmap) {
private fun shareQrBadge(badge: Bitmap?) {
if (badge == null) {
return
}
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)

View file

@ -1,7 +1,23 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
@ -10,8 +26,10 @@ import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.drawQr
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
@ -165,4 +183,106 @@ class UsernameLinkSettingsViewModel : ViewModel() {
url.map { QrCodeData.forData(it, 64) }
}
}
/**
* Fun fact: there's no way to draw a composable to a bitmap. You'd think there would be, but there isn't. You can "screenshot" it if it's 100% on-screen,
* but if it's partially offscreen you're SOL. So, we get to go through the fun process of re-drawing the QR badge to an image for sharing ourselves.
*
* Sizes were picked arbitrarily.
*
* I hate this as much as you do.
*/
fun generateQrCodeImage(): Bitmap? {
val state: UsernameLinkSettingsState = _state.value
if (state.qrCodeState !is QrCodeState.Present) {
Log.w(TAG, "Invalid state to generate QR code! ${state.qrCodeState.javaClass.simpleName}")
return null
}
val qrCodeData: QrCodeData = state.qrCodeState.data
val width = 480
val height = 525
val qrSize = 300f
val qrPadding = 25f
val borderSizeX = 64f
val borderSizeY = 52f
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
eraseColor(Color.TRANSPARENT)
}
val androidCanvas = android.graphics.Canvas(bitmap)
val composeCanvas = Canvas(androidCanvas)
val canvasDrawScope = CanvasDrawScope()
// Draw the background
androidCanvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), 30f, 30f, Paint().apply { color = state.qrCodeColorScheme.borderColor.toArgb() })
androidCanvas.drawRoundRect(borderSizeX, borderSizeY, borderSizeX + qrSize + qrPadding * 2, borderSizeY + qrSize + qrPadding * 2, 15f, 15f, Paint().apply { color = Color.WHITE })
androidCanvas.drawRoundRect(
borderSizeX,
borderSizeY,
borderSizeX + qrSize + qrPadding * 2,
borderSizeY + qrSize + qrPadding * 2,
15f,
15f,
Paint().apply {
color = state.qrCodeColorScheme.outlineColor.toArgb()
style = Paint.Style.STROKE
strokeWidth = 4f
}
)
// Draw the QR code
composeCanvas.translate((width / 2) - (qrSize / 2), 80f)
canvasDrawScope.draw(
density = object : Density {
override val density: Float = 1f
override val fontScale: Float = 1f
},
layoutDirection = LayoutDirection.Ltr,
canvas = composeCanvas,
size = Size(qrSize, qrSize)
) {
drawQr(
data = qrCodeData,
foregroundColor = state.qrCodeColorScheme.foregroundColor,
backgroundColor = state.qrCodeColorScheme.borderColor,
deadzonePercent = 0.35f,
logo = null
)
}
composeCanvas.translate(-90f, -80f)
// Draw the signal logo -- unfortunately can't have the normal QR code drawing handle it because it requires a composable ImageBitmap
BitmapFactory.decodeResource(ApplicationDependencies.getApplication().resources, R.drawable.qrcode_logo).also { logoBitmap ->
val tintedPaint = Paint().apply {
colorFilter = PorterDuffColorFilter(state.qrCodeColorScheme.foregroundColor.toArgb(), PorterDuff.Mode.SRC_IN)
}
val sourceRect = Rect(0, 0, logoBitmap.width, logoBitmap.height)
val destRect = RectF(210f, 200f, 270f, 260f)
androidCanvas.drawBitmap(logoBitmap, sourceRect, destRect, tintedPaint)
}
// Draw the text
val textPaint = Paint().apply {
color = state.qrCodeColorScheme.textColor.toArgb()
textSize = 34f
typeface = if (Build.VERSION.SDK_INT < 26) {
Typeface.DEFAULT_BOLD
} else {
Typeface.Builder("")
.setFallback("sans-serif")
.setWeight(600)
.build()
}
}
val textBounds = Rect()
textPaint.getTextBounds(state.username, 0, state.username.length, textBounds)
androidCanvas.drawText(state.username, (width / 2f) - (textBounds.width() / 2f), 465f, textPaint)
return bitmap
}
}

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.res.Configuration
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeDa
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
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.ScreenshotController
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@ -57,9 +55,8 @@ fun UsernameLinkShareScreen(
snackbarHostState: SnackbarHostState,
scope: CoroutineScope,
navController: NavController,
onShareBadge: (Bitmap) -> Unit,
onShareBadge: () -> Unit,
modifier: Modifier = Modifier,
screenshotController: ScreenshotController? = null,
onResetClicked: () -> Unit
) {
when (state.usernameLinkResetResult) {
@ -82,7 +79,6 @@ fun UsernameLinkShareScreen(
data = state.qrCodeState,
colorScheme = state.qrCodeColorScheme,
username = state.username,
screenshotController = screenshotController,
usernameCopyable = true,
modifier = Modifier.padding(horizontal = 58.dp, vertical = 24.dp),
onClick = {
@ -93,12 +89,7 @@ fun UsernameLinkShareScreen(
)
ButtonBar(
onShareClicked = {
val badgeBitmap = screenshotController?.screenshot()
if (badgeBitmap != null) {
onShareBadge.invoke(badgeBitmap)
}
},
onShareClicked = onShareBadge,
onColorClicked = { navController.safeNavigate(R.id.action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment) }
)