Update QrCodeActivity

This commit is contained in:
Andrew 2024-05-03 13:38:14 +09:30
parent ff64a8bc13
commit 18a4bcdbd9
13 changed files with 149 additions and 198 deletions

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
fun Context.threadDatabase() = DatabaseComponent.get(this).threadDatabase()

View File

@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.ui.components.OutlineButton
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.session_accent
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.util.start
class LandingActivity : BaseActionBarActivity() {
@ -105,7 +106,7 @@ class LandingActivity : BaseActionBarActivity() {
.width(262.dp)
.align(Alignment.CenterHorizontally)
.contentDescription(R.string.AccessibilityId_restore_account_button)
) { startLinkDeviceActivity() }
) { start<LinkDeviceActivity>() }
Spacer(modifier = Modifier.height(8.dp))
BorderlessButton(
text = stringResource(R.string.onboardingTosPrivacy),

View File

@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.onboarding
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.camera.core.ExperimentalGetImage
@ -160,10 +158,6 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
}
}
fun Context.startLinkDeviceActivity() {
Intent(this, LinkDeviceActivity::class.java).let(::startActivity)
}
class Analyzer(
private val scanner: BarcodeScanner,
private val onBarcodeScanned: (String) -> Unit

View File

@ -43,7 +43,7 @@ import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.classicDarkColors
import org.thoughtcrime.securesms.ui.colorDestructive
import org.thoughtcrime.securesms.ui.components.OutlineButton
import org.thoughtcrime.securesms.ui.components.QrImageCard
import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SmallButtons
import org.thoughtcrime.securesms.ui.components.TemporaryStateButton
import org.thoughtcrime.securesms.ui.contentDescription
@ -161,7 +161,7 @@ fun RecoveryPasswordCell(seed: String, copySeed:() -> Unit = {}) {
showQr,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
QrImageCard(
QrImage(
seed,
modifier = Modifier.padding(vertical = 24.dp),
contentDescription = "QR code of your recovery password",

View File

@ -1,141 +1,96 @@
package org.thoughtcrime.securesms.preferences
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityQrCodeBinding
import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.PublicKeyValidation
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.QRCodeUtilities
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.database.threadDatabase
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.setComposeContent
import org.thoughtcrime.securesms.ui.small
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.toPx
import java.io.File
import java.io.FileOutputStream
import org.thoughtcrime.securesms.util.start
private val TITLES = listOf(R.string.view, R.string.scan)
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
private lateinit var binding: ActivityQrCodeBinding
private val adapter = QRCodeActivityAdapter(this)
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
binding = ActivityQrCodeBinding.inflate(layoutInflater)
// Set content view
setContentView(binding.root)
// Set title
supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title)
// Set up view pager
binding.viewPager.adapter = adapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
}
// endregion
// region Interaction
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
createPrivateChatIfPossible(hexEncodedPublicKey)
setComposeContent {
Tabs(TextSecurePreferences.getLocalNumber(this)!!)
}
}
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show() }
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
val intent = Intent(this, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseComponent.get(this).threadDatabase().getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
startActivity(intent)
override fun handleQRCodeScanned(string: String) {
if (!PublicKeyValidation.isValid(string)) {
return Toast.makeText(this, R.string.invalid_session_id, Toast.LENGTH_SHORT).show()
}
val recipient = Recipient.from(this, Address.fromSerialized(string), false)
start<ConversationActivityV2> {
putExtra(ConversationActivityV2.ADDRESS, recipient.address)
setDataAndType(intent.data, intent.type)
val existingThread = threadDatabase().getThreadIdIfExistsFor(recipient)
putExtra(ConversationActivityV2.THREAD_ID, existingThread)
}
finish()
}
// endregion
}
// region Adapter
private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Tabs(sessionId: String) {
val pagerState = rememberPagerState { TITLES.size }
override fun getCount(): Int {
return 2
}
override fun getItem(index: Int): Fragment {
return when (index) {
0 -> ViewMyQRCodeFragment()
1 -> {
val result = ScanQRCodeWrapperFragment()
result.delegate = activity
result.message = activity.resources.getString(R.string.activity_qr_code_view_scan_qr_code_explanation)
result
Column {
SessionTabRow(pagerState, TITLES)
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f)
) { page ->
when (TITLES[page]) {
R.string.view -> QrPage(sessionId)
R.string.scan -> MaybeScanQrCode()
}
else -> throw IllegalStateException()
}
}
override fun getPageTitle(index: Int): CharSequence? {
return when (index) {
0 -> activity.resources.getString(R.string.activity_qr_code_view_my_qr_code_tab_title)
1 -> activity.resources.getString(R.string.activity_qr_code_view_scan_qr_code_tab_title)
else -> throw IllegalStateException()
}
}
}
// endregion
// region View My QR Code Fragment
class ViewMyQRCodeFragment : Fragment() {
private lateinit var binding: FragmentViewMyQrCodeBinding
@Composable
fun QrPage(string: String) {
Column(modifier = Modifier.padding(horizontal = 32.dp).fillMaxSize()) {
QrImage(
string = string,
contentDescription = "Your session id",
modifier = Modifier.padding(top = 32.dp, bottom = 12.dp),
icon = R.drawable.session
)
private val hexEncodedPublicKey: String
get() {
return TextSecurePreferences.getLocalNumber(requireContext())!!
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val size = toPx(280, resources)
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
binding.qrCodeImageView.setImageBitmap(qrCode)
// val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.")
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation)
binding.shareButton.setOnClickListener { shareQRCode() }
}
private fun shareQRCode() {
val directory = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val fileName = "$hexEncodedPublicKey.png"
val file = File(directory, fileName)
file.createNewFile()
val fos = FileOutputStream(file)
val size = toPx(280, resources)
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
qrCode.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, FileProviderUtil.getUriFor(requireActivity(), file))
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.type = "image/png"
startActivity(Intent.createChooser(intent, resources.getString(R.string.fragment_view_my_qr_code_share_title)))
Text(
text = "This is your Account ID. Other users can scan it to start a conversation with you.",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.small
)
}
}
// endregion

View File

@ -20,16 +20,12 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -40,18 +36,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.BuildConfig
@ -184,7 +175,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_qr_code -> {
showQRCode()
push<QRCodeActivity>()
true
}
else -> super.onOptionsItemSelected(item)
@ -325,11 +316,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
return true
}
private fun showQRCode() {
val intent = Intent(this, QRCodeActivity::class.java)
push(intent)
}
private fun showEditProfilePictureUI() {
showSessionDialog {
title(R.string.activity_settings_set_display_picture)

View File

@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.ui
import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
fun Activity.setComposeContent(content: @Composable () -> Unit) {
ComposeView(this)
.apply { setContent { AppTheme { content() } } }
.let(::setContentView)
}

View File

@ -58,13 +58,6 @@ typealias CameraPreview = androidx.camera.core.Preview
private const val TAG = "NewMessageFragment"
@Composable
fun MaybeScanQrCode(errors: Flow<String> = emptyFlow()) {
LocalContext.current.run {
MaybeScanQrCode(onScan = {})
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MaybeScanQrCode(

View File

@ -2,17 +2,15 @@ package org.thoughtcrime.securesms.ui.components
import android.graphics.Bitmap
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.size
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -34,21 +32,12 @@ import org.thoughtcrime.securesms.ui.LocalOnLightCell
import org.thoughtcrime.securesms.util.QRCodeUtilities
@Composable
fun QrImageCard(
fun QrImage(
string: String,
contentDescription: String,
modifier: Modifier = Modifier,
icon: Int = R.drawable.session_shield
) {
Card(
backgroundColor = LocalLightCell.current,
elevation = 0.dp,
modifier = modifier
) { QrImage(string, contentDescription, icon) }
}
@Composable
fun QrImage(string: String, contentDescription: String, icon: Int = R.drawable.session_shield) {
var bitmap: Bitmap? by remember {
mutableStateOf(null)
}
@ -56,42 +45,57 @@ fun QrImage(string: String, contentDescription: String, icon: Int = R.drawable.s
val scope = rememberCoroutineScope()
LaunchedEffect(string) {
scope.launch(Dispatchers.IO) {
bitmap = QRCodeUtilities.encode(string, 400)
bitmap = QRCodeUtilities.encode(string, 400).also {
for (y in 150 until 250) {
for (x in 150 until 250) {
it.setPixel(x, y, 0x00000000)
}
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
AnimatedVisibility(
visible = bitmap != null,
enter = scaleIn()
@Composable
fun content(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(1f)
) {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(LocalOnLightCell.current)
)
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)
)
}
Icon(
painter = painterResource(id = icon),
contentDescription = "",
tint = LocalOnLightCell.current,
modifier = Modifier
.align(Alignment.Center)
.width(46.dp)
.height(56.dp)
.background(color = LocalLightCell.current)
.padding(horizontal = 3.dp, vertical = 1.dp)
)
}
}
if (MaterialTheme.colors.isLight) {
content(modifier)
} else {
Card(
backgroundColor = LocalLightCell.current,
elevation = 0.dp,
modifier = modifier
) { content() }
}
}

View File

@ -102,6 +102,7 @@ data class ThemeState (
val followSystem: Boolean
)
inline fun <reified T: Activity> Context.start() = Intent(this, T::class.java).also(::startActivity)
inline fun <reified T: Activity> Activity.show() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) }
inline fun <reified T: Activity> Activity.push() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) }
inline fun <reified T: Activity> Context.start() = Intent(this, T::class.java).also(::startActivity)
inline fun <reified T: Activity> Context.start(modify: Intent.() -> Unit) = Intent(this, T::class.java).also(modify).also(::startActivity)

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="63dp"
android:viewportWidth="52"
android:viewportHeight="63">
<path
android:pathData="M43.22,31.788L32.683,25.284H39.825C42.812,25.284 45.676,23.961 47.789,21.608C49.901,19.254 51.087,16.062 51.087,12.734C51.087,9.405 49.901,6.213 47.789,3.86C45.676,1.506 42.812,0.184 39.825,0.184L14.888,0.184C11.148,0.189 7.562,1.847 4.917,4.794C2.272,7.742 0.784,11.738 0.779,15.906C0.781,18.943 1.512,21.925 2.897,24.542C4.282,27.159 6.27,29.315 8.654,30.787L19.193,37.293H12.042C10.552,37.274 9.073,37.584 7.692,38.206C6.31,38.828 5.053,39.75 3.994,40.917C2.934,42.084 2.093,43.474 1.518,45.006C0.944,46.538 0.648,48.182 0.648,49.842C0.648,51.503 0.944,53.146 1.518,54.679C2.093,56.211 2.934,57.6 3.994,58.768C5.053,59.935 6.31,60.856 7.692,61.478C9.073,62.1 10.552,62.41 12.042,62.391H36.979C40.719,62.387 44.305,60.729 46.951,57.781C49.596,54.834 51.083,50.838 51.087,46.67C51.087,43.633 50.357,40.651 48.973,38.034C47.59,35.418 45.604,33.261 43.22,31.788ZM10.338,27.386C8.541,26.275 7.035,24.659 5.971,22.7C4.907,20.74 4.322,18.504 4.273,16.215C4.132,9.538 9.163,4.072 15.156,4.072H39.599C43.781,4.072 47.381,7.638 47.589,12.297C47.64,13.47 47.478,14.642 47.111,15.742C46.745,16.842 46.181,17.848 45.455,18.699C44.729,19.549 43.856,20.226 42.888,20.69C41.92,21.153 40.878,21.392 39.825,21.393H25.748C25.336,21.396 24.941,21.58 24.65,21.906C24.359,22.233 24.196,22.674 24.197,23.134V35.937L10.338,27.386ZM36.71,58.503H12.267C8.088,58.503 4.485,54.937 4.28,50.279C4.228,49.107 4.39,47.935 4.756,46.835C5.123,45.735 5.686,44.729 6.412,43.878C7.138,43.027 8.011,42.35 8.979,41.886C9.946,41.423 10.988,41.183 12.042,41.182H26.12C26.325,41.182 26.528,41.137 26.717,41.049C26.906,40.962 27.078,40.834 27.223,40.672C27.368,40.511 27.482,40.319 27.561,40.108C27.639,39.897 27.679,39.671 27.679,39.443V26.639L41.528,35.19C43.327,36.301 44.834,37.917 45.898,39.879C46.962,41.84 47.547,44.078 47.595,46.369C47.736,53.037 42.705,58.503 36.71,58.503Z"
android:strokeWidth="0.3"
android:fillColor="#000000"
android:strokeColor="#000000"/>
</vector>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager.widget.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_bar_height" />
</androidx.viewpager.widget.ViewPager>

View File

@ -1094,6 +1094,8 @@
<string name="hide">Hide</string>
<string name="recoveryPasswordHideRecoveryPassword">Hide Recovery Password</string>
<string name="qrView">View QR</string>
<string name="view">View</string>
<string name="scan">Scan</string>
<string name="recoveryPasswordView">View Password</string>
<string name="recoveryPasswordHideRecoveryPasswordDescription">Permanently hide your recovery password on this device.</string>
<string name="conversationsNone">You don\'t have any conversations yet</string>