diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Util.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Util.kt new file mode 100644 index 0000000000..402ee6155f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Util.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index 75e96bd333..440b2d233d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -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() } Spacer(modifier = Modifier.height(8.dp)) BorderlessButton( text = stringResource(R.string.onboardingTosPrivacy), diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index db408fa9f7..78939adb4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt index 03c9c40d65..2cca42386e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt @@ -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", diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index 20f67cd29e..72745a820d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -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 { + 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 \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 6668ae7a3a..a0d96da0e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -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() 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt new file mode 100644 index 0000000000..ecaebb463d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Util.kt @@ -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) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index 0b314fecce..29aaeb4b9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -58,13 +58,6 @@ typealias CameraPreview = androidx.camera.core.Preview private const val TAG = "NewMessageFragment" -@Composable -fun MaybeScanQrCode(errors: Flow = emptyFlow()) { - LocalContext.current.run { - MaybeScanQrCode(onScan = {}) - } -} - @OptIn(ExperimentalPermissionsApi::class) @Composable fun MaybeScanQrCode( diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt index e36660340d..756c146292 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt @@ -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) - ) } -} \ No newline at end of file + + if (MaterialTheme.colors.isLight) { + content(modifier) + } else { + Card( + backgroundColor = LocalLightCell.current, + elevation = 0.dp, + modifier = modifier + ) { content() } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index b543b7d807..adb53889e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -102,6 +102,7 @@ data class ThemeState ( val followSystem: Boolean ) -inline fun Context.start() = Intent(this, T::class.java).also(::startActivity) inline fun Activity.show() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } inline fun Activity.push() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) } +inline fun Context.start() = Intent(this, T::class.java).also(::startActivity) +inline fun Context.start(modify: Intent.() -> Unit) = Intent(this, T::class.java).also(modify).also(::startActivity) diff --git a/app/src/main/res/drawable/session.xml b/app/src/main/res/drawable/session.xml new file mode 100644 index 0000000000..2c6e1081f7 --- /dev/null +++ b/app/src/main/res/drawable/session.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/activity_qr_code.xml b/app/src/main/res/layout/activity_qr_code.xml deleted file mode 100644 index 58c7e40c82..0000000000 --- a/app/src/main/res/layout/activity_qr_code.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5723f70ba..dfe29aec40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1094,6 +1094,8 @@ Hide Hide Recovery Password View QR + View + Scan View Password Permanently hide your recovery password on this device. You don\'t have any conversations yet