diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt index a0eab206e..42fc83f05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView 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 dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R @@ -38,6 +39,8 @@ import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.notifications.PushRegistry import org.thoughtcrime.securesms.ui.AppTheme import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider import org.thoughtcrime.securesms.ui.h8 import org.thoughtcrime.securesms.ui.h9 import org.thoughtcrime.securesms.ui.session_accent @@ -66,7 +69,9 @@ class MessageNotificationsActivity : BaseActionBarActivity() { private fun MessageNotifications() { val state by viewModel.stateFlow.collectAsState() - MessageNotifications(state, viewModel::setEnabled, ::register) + AppTheme { + MessageNotifications(state, viewModel::setEnabled, ::register) + } } private fun register() { @@ -81,43 +86,50 @@ class MessageNotificationsActivity : BaseActionBarActivity() { } @Preview +@Composable +fun MessageNotificationsPreview( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + MessageNotifications() + } +} + @Composable fun MessageNotifications( state: MessageNotificationsState = MessageNotificationsState(), setEnabled: (Boolean) -> Unit = {}, onContinue: () -> Unit = {} ) { - AppTheme { - Column(Modifier.padding(horizontal = 32.dp)) { - Spacer(Modifier.weight(1f)) - Text("Message notifications", style = MaterialTheme.typography.h4) - Spacer(Modifier.height(16.dp)) - Text("There are two ways Session can notify you of new messages.") - Spacer(Modifier.height(16.dp)) - NotificationRadioButton( - R.string.activity_pn_mode_fast_mode, - R.string.activity_pn_mode_fast_mode_explanation, - R.string.activity_pn_mode_recommended_option_tag, - selected = state.pushEnabled, - onClick = { setEnabled(true) } - ) - Spacer(Modifier.height(16.dp)) - NotificationRadioButton( - R.string.activity_pn_mode_slow_mode, - R.string.activity_pn_mode_slow_mode_explanation, - selected = state.pushDisabled, - onClick = { setEnabled(false) } - ) - Spacer(Modifier.weight(1f)) - OutlineButton( - stringResource(R.string.continue_2), - modifier = Modifier - .align(Alignment.CenterHorizontally) - .width(262.dp), - onClick = onContinue - ) - Spacer(modifier = Modifier.height(12.dp)) - } + Column(Modifier.padding(horizontal = 32.dp)) { + Spacer(Modifier.weight(1f)) + Text("Message notifications", style = MaterialTheme.typography.h4) + Spacer(Modifier.height(16.dp)) + Text("There are two ways Session can notify you of new messages.") + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_fast_mode, + R.string.activity_pn_mode_fast_mode_explanation, + R.string.activity_pn_mode_recommended_option_tag, + selected = state.pushEnabled, + onClick = { setEnabled(true) } + ) + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_slow_mode, + R.string.activity_pn_mode_slow_mode_explanation, + selected = state.pushDisabled, + onClick = { setEnabled(false) } + ) + Spacer(Modifier.weight(1f)) + OutlineButton( + stringResource(R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(262.dp), + onClick = onContinue + ) + Spacer(modifier = Modifier.height(12.dp)) } } 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 82b0bd33c..ab1db3e96 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 @@ -4,22 +4,39 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box 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.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview @@ -33,7 +50,10 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.CellNoMargin import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.LocalExtraColors import org.thoughtcrime.securesms.ui.OutlineButton import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.SessionShieldIcon @@ -42,27 +62,21 @@ import org.thoughtcrime.securesms.ui.classicDarkColors import org.thoughtcrime.securesms.ui.colorDestructive import org.thoughtcrime.securesms.ui.extraSmall import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.small class RecoveryPasswordActivity : BaseActionBarActivity() { - private val seed by lazy { - var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) - if (hexEncodedSeed == null) { - hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account - } - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) - } + private val viewModel: RecoveryPasswordViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar!!.title = resources.getString(R.string.activity_recovery_password) - ComposeView(this) - .apply { setContent { RecoveryPassword() } } - .let(::setContentView) + ComposeView(this).apply { + setContent { + RecoveryPassword(viewModel.seed, viewModel.bitmap) { copySeed() } + } + }.let(::setContentView) } private fun revealSeed() { @@ -72,7 +86,7 @@ class RecoveryPasswordActivity : BaseActionBarActivity() { private fun copySeed() { revealSeed() val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Seed", seed) + val clip = ClipData.newPlainText("Seed", viewModel.seed) clipboard.setPrimaryClip(clip) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } @@ -84,23 +98,30 @@ fun PreviewMessageDetails( @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int ) { PreviewTheme(themeResId) { - RecoveryPassword() + RecoveryPassword(seed = "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane") } } @Composable -fun RecoveryPassword() { +fun RecoveryPassword(seed: String = "", bitmap: Bitmap? = null, copySeed:() -> Unit = {}) { AppTheme { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - RecoveryPasswordCell() + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp) + ) { + RecoveryPasswordCell(seed, bitmap, copySeed) HideRecoveryPasswordCell() } } } - @Composable -fun RecoveryPasswordCell() { +fun RecoveryPasswordCell(seed: String = "", bitmap: Bitmap? = null, copySeed:() -> Unit = {}) { + val showQr = remember { + mutableStateOf(false) + } + CellWithPaddingAndMargin { Column { Row { @@ -111,27 +132,71 @@ fun RecoveryPasswordCell() { Text("Use your recovery password to load your account on new devices.\n\nYour account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.") - Text( - "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane", - modifier = Modifier - .padding(vertical = 24.dp) - .border( - width = 1.dp, - color = classicDarkColors[3], - shape = RoundedCornerShape(11.dp) - ) - .padding(24.dp), - style = MaterialTheme.typography.extraSmall.copy(fontFamily = FontFamily.Monospace) - ) + AnimatedVisibility(!showQr.value) { + Text( + seed, + modifier = Modifier + .padding(vertical = 24.dp) + .border( + width = 1.dp, + color = classicDarkColors[3], + shape = RoundedCornerShape(11.dp) + ) + .padding(24.dp), + style = MaterialTheme.typography.small.copy(fontFamily = FontFamily.Monospace), + color = LocalExtraColors.current.prominentButtonColor, + ) + } - Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { - OutlineButton(text = stringResource(R.string.copy), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) {} - OutlineButton(text = "View QR", modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) {} + AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) { + Card( + backgroundColor = Color.White, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 24.dp) + ) { + Box { + bitmap?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "some useful description", + ) + } + + Icon( + painter = painterResource(id = R.drawable.session_shield), + contentDescription = "", + tint = Color.Black, + modifier = Modifier.align(Alignment.Center) + .width(46.dp) + .height(56.dp) + .background(color = Color.White) + .padding(horizontal = 3.dp, vertical = 1.dp) + ) + } + } + } + + AnimatedVisibility(!showQr.value) { + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + OutlineButton(text = stringResource(R.string.copy), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { copySeed() } + OutlineButton(text = "View QR", modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() } + } + } + + AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) { + OutlineButton( + text = "View Password", + color = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { showQr.toggle() } } } } } +private fun MutableState.toggle() { value = !value } + @Composable fun HideRecoveryPasswordCell() { CellWithPaddingAndMargin { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt new file mode 100644 index 000000000..190571a5c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.onboarding.recoverypassword + +import android.app.Application +import android.graphics.Bitmap +import androidx.lifecycle.AndroidViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.QRCodeUtilities +import org.thoughtcrime.securesms.util.toPx +import javax.inject.Inject + +@HiltViewModel +class RecoveryPasswordViewModel @Inject constructor( + private val application: Application +): AndroidViewModel(application) { + + val bitmap: Bitmap? = TextSecurePreferences.getLocalNumber(application)?.let { + QRCodeUtilities.encode( + data = it, + size = toPx(280, application.resources), + isInverted = false, + hasTransparentBackground = true + ) + } + + val seed by lazy { + val hexEncodedSeed = IdentityKeyUtil.retrieve(application, IdentityKeyUtil.LOKI_SEED) + ?: IdentityKeyUtil.getIdentityKeyPair(application).hexEncodedPrivateKey // Legacy account + MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) } + .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt index f7d1e3e8a..d70bb3be8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt @@ -9,17 +9,26 @@ import com.google.zxing.qrcode.QRCodeWriter object QRCodeUtilities { - fun encode(data: String, size: Int, isInverted: Boolean = false, hasTransparentBackground: Boolean = true): Bitmap { + fun encode( + data: String, + size: Int, + isInverted: Boolean = false, + hasTransparentBackground: Boolean = true, + dark: Int = Color.BLACK, + light: Int = Color.WHITE, + ): Bitmap { try { val hints = hashMapOf( EncodeHintType.MARGIN to 1 ) val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints) val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) + val color = if (isInverted) light else dark + val background = if (isInverted) dark else light for (y in 0 until result.height) { for (x in 0 until result.width) { if (result.get(x, y)) { - bitmap.setPixel(x, y, if (isInverted) Color.WHITE else Color.BLACK) + bitmap.setPixel(x, y, color) } else if (!hasTransparentBackground) { - bitmap.setPixel(x, y, if (isInverted) Color.BLACK else Color.WHITE) + bitmap.setPixel(x, y, background) } } }