Add sharing for PNP usernames badge.

This commit is contained in:
Clark
2023-06-13 16:00:22 -04:00
committed by Cody Henthorne
parent cff01021c2
commit b9835584d8
4 changed files with 150 additions and 10 deletions

View File

@@ -15,30 +15,44 @@ 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.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.compose.ScreenshotController
import org.thoughtcrime.securesms.compose.getScreenshotBounds
/**
* Renders a QR code and username as a badge.
*/
@Composable
fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, username: String, modifier: Modifier = Modifier) {
fun QrCodeBadge(data: QrCodeData?, colorScheme: UsernameQrCodeColorScheme, username: String, modifier: Modifier = Modifier, screenshotController: ScreenshotController? = null) {
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor)
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor)
val elevation by animateFloatAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) 10f else 0f)
val textColor by animateColorAsState(targetValue = if (colorScheme == UsernameQrCodeColorScheme.White) Color.Black else Color.White)
var badgeBounds by remember {
mutableStateOf<Rect?>(null)
}
screenshotController?.bind(LocalView.current, badgeBounds)
Surface(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 59.dp, vertical = 24.dp),
.padding(horizontal = 59.dp, vertical = 24.dp)
.onGloballyPositioned {
badgeBounds = it.getScreenshotBounds()
},
color = borderColor,
shape = RoundedCornerShape(24.dp),
shadowElevation = elevation.dp

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import androidx.compose.animation.AnimatedVisibility
@@ -32,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
@@ -47,6 +50,9 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.ScreenshotController
import org.thoughtcrime.securesms.providers.BlobProvider
import java.io.ByteArrayOutputStream
@OptIn(
ExperimentalMaterial3Api::class,
@@ -57,6 +63,8 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
private val disposables: LifecycleDisposable = LifecycleDisposable()
private val screenshotController = ScreenshotController()
@Composable
override fun FragmentContent() {
val state by viewModel.state
@@ -83,7 +91,11 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
snackbarHostState = snackbarHostState,
scope = scope,
modifier = Modifier.padding(contentPadding),
navController = navController
navController = navController,
onShareBadge = {
shareQrBadge(it)
},
screenshotController = screenshotController
)
}
@@ -185,4 +197,29 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
fun PreviewAll() {
FragmentContent()
}
private fun shareQrBadge(badge: Bitmap) {
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
byteArrayOutputStream.flush()
val bytes = byteArrayOutputStream.toByteArray()
val shareUri = BlobProvider.getInstance()
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalGroupQr.png")
.createForSingleSessionInMemory()
val intent = ShareCompat.IntentBuilder.from(requireActivity())
.setType("image/png")
.setStream(shareUri)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
}
} finally {
badge.recycle()
}
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -36,6 +37,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeBa
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeData
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.UsernameUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -49,7 +51,9 @@ fun UsernameLinkShareScreen(
snackbarHostState: SnackbarHostState,
scope: CoroutineScope,
navController: NavController,
modifier: Modifier = Modifier
onShareBadge: (Bitmap) -> Unit,
modifier: Modifier = Modifier,
screenshotController: ScreenshotController? = null
) {
Column(
modifier = modifier
@@ -58,10 +62,17 @@ fun UsernameLinkShareScreen(
QrCodeBadge(
data = state.qrCodeData,
colorScheme = state.qrCodeColorScheme,
username = state.username
username = state.username,
screenshotController = screenshotController
)
ButtonBar(
onShareClicked = {
val badgeBitmap = screenshotController?.screenshot()
if (badgeBitmap != null) {
onShareBadge.invoke(badgeBitmap)
}
},
onColorClicked = { navController.safeNavigate(R.id.action_usernameLinkSettingsFragment_to_usernameLinkQrColorPickerFragment) }
)
@@ -103,13 +114,13 @@ fun UsernameLinkShareScreen(
}
@Composable
private fun ButtonBar(onColorClicked: () -> Unit) {
private fun ButtonBar(onShareClicked: () -> Unit, onColorClicked: () -> Unit) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 32.dp, alignment = Alignment.CenterHorizontally),
modifier = Modifier.fillMaxWidth()
) {
Buttons.ActionButton(
onClick = {},
onClick = onShareClicked,
iconResId = R.drawable.symbol_share_android_24,
labelResId = R.string.UsernameLinkSettings_share_button_label
)
@@ -161,7 +172,8 @@ private fun ScreenPreviewLightTheme() {
state = previewState(),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current)
navController = NavController(LocalContext.current),
onShareBadge = {}
)
}
}
@@ -176,7 +188,8 @@ private fun ScreenPreviewDarkTheme() {
state = previewState(),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope(),
navController = NavController(LocalContext.current)
navController = NavController(LocalContext.current),
onShareBadge = {}
)
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.compose
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.PixelCopy
import android.view.View
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
/**
* Helper class for screenshotting compose views.
*
* You need to call bind from the compose, passing in the
* LocalView.current view with bounds fetched from when the
* composable is globally positioned.
*
* See QrCodeBadge.kt for an example
*/
class ScreenshotController {
private var screenshotCallback: (() -> Bitmap?)? = null
fun bind(view: View, bounds: Rect?) {
if (bounds == null) {
screenshotCallback = null
return
}
screenshotCallback = {
val bitmap = Bitmap.createBitmap(
bounds.width.toInt(),
bounds.height.toInt(),
Bitmap.Config.ARGB_8888
)
if (Build.VERSION.SDK_INT >= 26) {
PixelCopy.request(
(view.context as Activity).window,
android.graphics.Rect(bounds.left.toInt(), bounds.top.toInt(), bounds.right.toInt(), bounds.bottom.toInt()),
bitmap,
{},
Handler(Looper.getMainLooper())
)
} else {
val canvas = Canvas(bitmap)
.apply {
translate(-bounds.left, -bounds.top)
}
view.draw(canvas)
}
bitmap
}
}
fun screenshot(): Bitmap? {
return screenshotCallback?.invoke()
}
}
fun LayoutCoordinates.getScreenshotBounds(): Rect {
return if (Build.VERSION.SDK_INT >= 26) {
this.boundsInWindow()
} else {
this.boundsInRoot()
}
}