mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Add sharing for PNP usernames badge.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user