mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-04 14:42:00 +00:00
android: passcode implementation (#2177)
* android: passcode implementation * layout * passcode view * unused param * text for auth * small changes * fix * use preference instead of toggle * removed useless code and changed title of auth screen * removed unneeded function * EOLs * changed local variable logic to global variable * formatting * different alert * changed code placement * alert behaviour * button size * tint of buttons * error instead of failed status * do not show auth alerts on failures, only on final errors --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c2d70a5107
commit
ead67adeb8
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import SectionItemView
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -12,8 +13,7 @@ import androidx.activity.viewModels
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
@@ -21,12 +21,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.MainActivity.Companion.enteredBackground
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
@@ -37,8 +38,11 @@ import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.app.views.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import chat.simplex.app.views.usersettings.LAMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@@ -93,7 +97,7 @@ class MainActivity: FragmentActivity() {
|
||||
laFailed,
|
||||
::runAuthenticate,
|
||||
::setPerformLA,
|
||||
showLANotice = { m.controller.showLANotice(this) }
|
||||
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -111,7 +115,8 @@ class MainActivity: FragmentActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
|
||||
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
@@ -165,16 +170,27 @@ class MainActivity: FragmentActivity() {
|
||||
delay(50)
|
||||
withContext(Dispatchers.Main) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_unlock),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_unlock)
|
||||
else
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_log_in_using_credential)
|
||||
else
|
||||
generalGetString(R.string.auth_unlock),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
userAuthorized.value = true
|
||||
is LAResult.Error, LAResult.Failed ->
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
laFailed.value = true
|
||||
LAResult.Unavailable -> {
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) {
|
||||
laFailedAlert()
|
||||
}
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
@@ -188,21 +204,116 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA()
|
||||
} else {
|
||||
disableLA()
|
||||
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.la_notice_title_simplex_lock),
|
||||
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
|
||||
confirmText = generalGetString(R.string.la_notice_turn_on),
|
||||
onConfirm = {
|
||||
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
|
||||
showChooseLAMode(laNoticeShown, activity)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA() {
|
||||
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.la_lock_mode),
|
||||
text = null,
|
||||
confirmText = generalGetString(R.string.la_lock_mode_passcode),
|
||||
dismissText = generalGetString(R.string.la_lock_mode_system),
|
||||
onConfirm = {
|
||||
AlertManager.shared.hideAlert()
|
||||
setPasscode()
|
||||
},
|
||||
onDismiss = {
|
||||
AlertManager.shared.hideAlert()
|
||||
initialEnableLA(activity)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialEnableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
val appPrefs = m.controller.appPrefs
|
||||
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_enable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setPasscode() {
|
||||
val chatModel = vm.chatModel
|
||||
val appPrefs = chatModel.controller.appPrefs
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
chatModel.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
appPrefs.laMode.set(LAMode.PASSCODE)
|
||||
laTurnedOnAlert()
|
||||
},
|
||||
cancel = {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA(activity)
|
||||
} else {
|
||||
disableLA(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_enable_simplex_lock)
|
||||
else
|
||||
generalGetString(R.string.new_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
@@ -211,11 +322,13 @@ class MainActivity: FragmentActivity() {
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
@@ -225,24 +338,33 @@ class MainActivity: FragmentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun disableLA() {
|
||||
private fun disableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_disable_simplex_lock)
|
||||
else
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
ksAppPassword.remove()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedAlert()
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
@@ -264,7 +386,7 @@ fun MainPage(
|
||||
userAuthorized: MutableState<Boolean?>,
|
||||
laFailed: MutableState<Boolean>,
|
||||
runAuthenticate: () -> Unit,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit,
|
||||
showLANotice: () -> Unit
|
||||
) {
|
||||
var showChatDatabaseError by rememberSaveable {
|
||||
@@ -392,6 +514,14 @@ fun MainPage(
|
||||
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
|
||||
AlertManager.shared.showInView()
|
||||
}
|
||||
|
||||
DisposableEffectOnRotate {
|
||||
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
|
||||
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
|
||||
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
|
||||
enteredBackground.value = elapsedRealtime() + 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
|
||||
@@ -326,4 +326,4 @@ class SimplexService: Service() {
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -84,6 +83,8 @@ class AppPreferences(val context: Context) {
|
||||
set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) }
|
||||
)
|
||||
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
|
||||
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } }
|
||||
val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30)
|
||||
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
|
||||
@@ -142,6 +143,8 @@ class AppPreferences(val context: Context) {
|
||||
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
|
||||
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
||||
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
|
||||
val encryptedAppPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE, null)
|
||||
val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null)
|
||||
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
|
||||
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
|
||||
|
||||
@@ -184,6 +187,12 @@ class AppPreferences(val context: Context) {
|
||||
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply()
|
||||
)
|
||||
|
||||
private fun <T> mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference<T> =
|
||||
SharedPreference(
|
||||
get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default,
|
||||
set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply()
|
||||
)
|
||||
|
||||
/**
|
||||
* Provide `[commit] = true` to save preferences right now, not after some unknown period of time.
|
||||
* So in case of a crash this value will be saved 100%
|
||||
@@ -210,6 +219,8 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
|
||||
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
|
||||
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
|
||||
private const val SHARED_PREFS_LA_MODE = "LocalAuthenticationMode"
|
||||
private const val SHARED_PREFS_LA_LOCK_DELAY = "LocalAuthenticationLockDelay"
|
||||
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
|
||||
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
|
||||
private const val SHARED_PREFS_PRIVACY_PROTECT_SCREEN = "PrivacyProtectScreen"
|
||||
@@ -246,6 +257,8 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE = "EncryptedAppPassphrase"
|
||||
private const val SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE = "InitializationVectorAppPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
@@ -1707,43 +1720,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
)
|
||||
}
|
||||
|
||||
fun showLANotice(activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!appPrefs.laNoticeShown.get()) {
|
||||
appPrefs.laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.la_notice_title_simplex_lock),
|
||||
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
|
||||
confirmText = generalGetString(R.string.la_notice_turn_on),
|
||||
onConfirm = {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_enable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
chatModel.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
chatModel.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
|
||||
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager
|
||||
return powerManager.isIgnoringBatteryOptimizations(context.packageName)
|
||||
@@ -3669,4 +3645,4 @@ sealed class XFTPErrorType {
|
||||
@Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType()
|
||||
@Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType()
|
||||
@Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -74,4 +74,4 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,4 +148,4 @@ fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester?
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(HighOrLowlight)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
|
||||
chatModel.controller.ntfManager.cancelCallNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,4 @@ class SoundPlayer {
|
||||
companion object {
|
||||
val shared = SoundPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String {
|
||||
return (0..(s.length - 1) / length)
|
||||
.map { s.drop(it * length).take(length) }
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
|
||||
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//}
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -36,7 +37,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
|
||||
+5
-5
@@ -42,9 +42,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") }
|
||||
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") }
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||
val newKey = rememberSaveable { mutableStateOf("") }
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
|
||||
@@ -89,7 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
prefs.initialRandomDBPassphrase.set(false)
|
||||
initialRandomDBPassphrase.value = false
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.setDatabaseKey(newKey.value)
|
||||
DatabaseUtils.ksDatabasePassword.set(newKey.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
@@ -150,7 +150,7 @@ fun DatabaseEncryptionLayout(
|
||||
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.remove_passphrase),
|
||||
onConfirm = {
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
},
|
||||
@@ -522,4 +522,4 @@ fun PreviewDatabaseEncryptionLayout() {
|
||||
onConfirmEncrypt = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ fun DatabaseErrorView(
|
||||
) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val dbKey = remember { mutableStateOf("") }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val context = LocalContext.current
|
||||
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
|
||||
@@ -49,7 +49,7 @@ fun DatabaseErrorView(
|
||||
}
|
||||
|
||||
fun saveAndRunChatOnClick() {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
DatabaseUtils.ksDatabasePassword.set(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
useKeychain = true
|
||||
|
||||
@@ -52,7 +52,7 @@ fun DatabaseView(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) }
|
||||
val runChat = remember { m.chatRunning }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
|
||||
@@ -76,7 +76,7 @@ fun DatabaseView(
|
||||
) {
|
||||
DatabaseLayout(
|
||||
progressIndicator.value,
|
||||
runChat.value,
|
||||
runChat.value != false,
|
||||
m.chatDbChanged.value,
|
||||
useKeychain.value,
|
||||
m.chatDbEncrypted.value,
|
||||
@@ -388,7 +388,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
||||
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
|
||||
}
|
||||
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
withApi {
|
||||
try {
|
||||
if (chatDbChanged.value) {
|
||||
@@ -417,7 +417,7 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStar
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.stop_chat_question),
|
||||
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
|
||||
@@ -434,7 +434,7 @@ private fun exportProhibitedAlert() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
if (m.controller.appPrefs.performLA.get()) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_stop_chat),
|
||||
@@ -442,12 +442,13 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
stopChat(m, runChat, context)
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
runChat.value = true
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
is LAResult.Failed -> {
|
||||
runChat.value = true
|
||||
}
|
||||
}
|
||||
@@ -458,7 +459,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
withApi {
|
||||
try {
|
||||
m.controller.apiStopChat()
|
||||
@@ -592,7 +593,7 @@ private fun importArchive(
|
||||
try {
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
|
||||
m.controller.apiImportArchive(config)
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
|
||||
@@ -647,7 +648,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
|
||||
try {
|
||||
m.controller.apiDeleteStorage()
|
||||
m.chatDbDeleted.value = true
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
|
||||
import android.util.Log
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.model.SharedPreference
|
||||
import chat.simplex.app.views.usersettings.Cryptor
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
@@ -16,30 +17,36 @@ object DatabaseUtils {
|
||||
}
|
||||
|
||||
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
|
||||
private const val APP_PASSWORD_ALIAS: String = "appPassword"
|
||||
|
||||
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
|
||||
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
||||
|
||||
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
||||
fun get(): String? {
|
||||
return cryptor.decryptData(
|
||||
passphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
initVector.get()?.toByteArrayFromBase64() ?: return null,
|
||||
alias,
|
||||
)
|
||||
}
|
||||
|
||||
fun set(key: String) {
|
||||
val data = cryptor.encryptText(key, alias)
|
||||
passphrase.set(data.first.toBase64String())
|
||||
initVector.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
cryptor.deleteKey(alias)
|
||||
passphrase.set(null)
|
||||
initVector.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasDatabase(rootDir: String): Boolean =
|
||||
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
|
||||
|
||||
fun getDatabaseKey(): String? {
|
||||
return cryptor.decryptData(
|
||||
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
DATABASE_PASSWORD_ALIAS,
|
||||
)
|
||||
}
|
||||
|
||||
fun setDatabaseKey(key: String) {
|
||||
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
|
||||
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun removeDatabaseKey() {
|
||||
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(null)
|
||||
appPreferences.initializationVectorDBPassphrase.set(null)
|
||||
}
|
||||
|
||||
fun useDatabaseKey(): String {
|
||||
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
|
||||
var dbKey = ""
|
||||
@@ -47,10 +54,10 @@ object DatabaseUtils {
|
||||
if (useKeychain) {
|
||||
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
|
||||
dbKey = randomDatabasePassword()
|
||||
setDatabaseKey(dbKey)
|
||||
ksDatabasePassword.set(dbKey)
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
} else {
|
||||
dbKey = getDatabaseKey() ?: ""
|
||||
dbKey = ksDatabasePassword.get() ?: ""
|
||||
}
|
||||
}
|
||||
return dbKey
|
||||
@@ -101,4 +108,4 @@ data class UpMigration(
|
||||
sealed class MTRError {
|
||||
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
|
||||
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -225,4 +225,4 @@ fun DefaultConfigurableTextField(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+56
-16
@@ -1,36 +1,66 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.LocalAuthView
|
||||
import chat.simplex.app.views.usersettings.LAMode
|
||||
|
||||
sealed class LAResult {
|
||||
object Success: LAResult()
|
||||
class Error(val errString: CharSequence): LAResult()
|
||||
object Failed: LAResult()
|
||||
object Unavailable: LAResult()
|
||||
class Failed(val errString: CharSequence? = null): LAResult()
|
||||
class Unavailable(val errString: CharSequence? = null): LAResult()
|
||||
}
|
||||
|
||||
data class LocalAuthRequest (
|
||||
val title: String?,
|
||||
val reason: String,
|
||||
val password: String,
|
||||
val completed: (LAResult) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
promptTitle: String,
|
||||
promptSubtitle: String,
|
||||
activity: FragmentActivity,
|
||||
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
when {
|
||||
SDK_INT in 28..29 ->
|
||||
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
SDK_INT > 29 ->
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
|
||||
else ->
|
||||
completed(LAResult.Unavailable)
|
||||
when (usingLAMode) {
|
||||
LAMode.SYSTEM -> when {
|
||||
SDK_INT in 28..29 ->
|
||||
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
SDK_INT > 29 ->
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
|
||||
close()
|
||||
completed(it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +96,7 @@ private fun authenticateWithBiometricManager(
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
completed(LAResult.Failed)
|
||||
completed(LAResult.Failed())
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -78,9 +108,7 @@ private fun authenticateWithBiometricManager(
|
||||
.build()
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
else -> {
|
||||
completed(LAResult.Unavailable)
|
||||
}
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +117,18 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
|
||||
)
|
||||
|
||||
fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.lock_not_enabled),
|
||||
generalGetString(R.string.you_can_turn_on_lock)
|
||||
)
|
||||
|
||||
fun laFailedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.la_auth_failed),
|
||||
text = generalGetString(R.string.la_could_not_be_verified)
|
||||
)
|
||||
}
|
||||
|
||||
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_unavailable),
|
||||
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)
|
||||
|
||||
@@ -640,4 +640,20 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
always()
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
val orientation = activity.resources.configuration.orientation
|
||||
onDispose {
|
||||
whenDispose()
|
||||
if (orientation != activity.resources.configuration.orientation) {
|
||||
whenRotate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
||||
val passcode = rememberSaveable { mutableStateOf("") }
|
||||
PasscodeView(passcode, authRequest.title ?: stringResource(R.string.la_enter_app_passcode), authRequest.reason, stringResource(R.string.submit_passcode),
|
||||
submit = {
|
||||
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode))
|
||||
authRequest.completed(r)
|
||||
},
|
||||
cancel = {
|
||||
authRequest.completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun PasscodeView(
|
||||
passcode: MutableState<String>,
|
||||
title: String,
|
||||
reason: String? = null,
|
||||
submitLabel: String,
|
||||
submitEnabled: ((String) -> Boolean)? = null,
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
@Composable
|
||||
fun VerticalLayout() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(title, style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, true)
|
||||
Row {
|
||||
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizontalLayout() {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) {
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(title, style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, false)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Just to fill space to correctly calculate the height
|
||||
Column {
|
||||
Text("", style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text("", Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
PasscodeView(remember { mutableStateOf("") })
|
||||
}
|
||||
BoxWithConstraints {
|
||||
val s = minOf(maxWidth, maxHeight) / 3.5f
|
||||
Column(
|
||||
Modifier.padding(start = 30.dp).height(s * 3),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
|
||||
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
VerticalLayout()
|
||||
} else {
|
||||
HorizontalLayout()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Backspace
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
|
||||
@Composable
|
||||
fun PasscodeEntry(
|
||||
password: MutableState<String>,
|
||||
vertical: Boolean,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PasscodeView(password)
|
||||
BoxWithConstraints {
|
||||
if (vertical) {
|
||||
VerticalPasswordGrid(password)
|
||||
} else {
|
||||
HorizontalPasswordGrid(password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PasscodeView(password: MutableState<String>) {
|
||||
var showPasscode by rememberSaveable { mutableStateOf(false) }
|
||||
Text(
|
||||
if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
|
||||
Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode },
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) {
|
||||
val s = minOf(maxWidth, maxHeight) / 4 - 1.dp
|
||||
Column(Modifier.width(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 1, 2, 3, password)
|
||||
Divider()
|
||||
DigitsRow(s, 4, 5, 6, password)
|
||||
Divider()
|
||||
DigitsRow(s, 7, 8, 9, password)
|
||||
Divider()
|
||||
Row(Modifier.requiredHeight(s)) {
|
||||
PasswordEdit(s, Icons.Default.Close) {
|
||||
password.value = ""
|
||||
}
|
||||
VerticalDivider()
|
||||
PasswordDigit(s, 0, password)
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Outlined.Backspace) {
|
||||
password.value = password.value.dropLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.HorizontalPasswordGrid(password: MutableState<String>) {
|
||||
val s = minOf(maxWidth, maxHeight) / 3.5f - 1.dp
|
||||
Column(Modifier.width(IntrinsicSize.Min)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 1, 2, 3, password);
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Default.Close) {
|
||||
password.value = ""
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 4, 5, 6, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(s, 0, password)
|
||||
}
|
||||
Divider()
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 7, 8, 9, password)
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Outlined.Backspace) {
|
||||
password.value = password.value.dropLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitPassword(showPassword: Boolean, password: String): String {
|
||||
val n = if (password.length < 8) 8 else 4
|
||||
return password.mapIndexed { index, c -> (if (showPassword) c.toString() else "●") + (if ((index + 1) % n == 0) " " else "") }.joinToString("")
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DigitsRow(size: Dp, d1: Int, d2: Int, d3: Int, password: MutableState<String>) {
|
||||
Row(Modifier.height(size)) {
|
||||
PasswordDigit(size, d1, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(size, d2, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(size, d3, password)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordDigit(size: Dp, d: Int, password: MutableState<String>) {
|
||||
val s = d.toString()
|
||||
return PasswordButton(size, action = {
|
||||
if (password.value.length < 16) {
|
||||
password.value += s
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
s,
|
||||
style = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 30.sp,
|
||||
letterSpacing = (-0.5).sp
|
||||
),
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordEdit(size: Dp, image: ImageVector, action: () -> Unit) {
|
||||
PasswordButton(size, action) {
|
||||
Icon(image, null, tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordButton(size: Dp, action: () -> Unit, content: @Composable BoxScope.() -> Unit) {
|
||||
return Box(
|
||||
Modifier.size(size)
|
||||
.background(MaterialTheme.colors.background, RoundedCornerShape(50))
|
||||
.clickable { action() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),
|
||||
thickness: Dp = 1.dp,
|
||||
startIndent: Dp = 0.dp
|
||||
) {
|
||||
val indentMod = if (startIndent.value != 0f) {
|
||||
Modifier.padding(top = startIndent)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
val targetThickness = if (thickness == Dp.Hairline) {
|
||||
(1f / LocalDensity.current.density).dp
|
||||
} else {
|
||||
thickness
|
||||
}
|
||||
Box(
|
||||
modifier.then(indentMod)
|
||||
.fillMaxHeight()
|
||||
.width(targetThickness)
|
||||
.background(color = color)
|
||||
)
|
||||
}
|
||||
|
||||
private const val DividerAlpha = 0.12f
|
||||
@@ -0,0 +1,48 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun SetAppPasscodeView(
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val passcode = rememberSaveable { mutableStateOf("") }
|
||||
var enteredPassword by rememberSaveable { mutableStateOf("") }
|
||||
var confirming by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun SetPasswordView(title: String, submitLabel: String, submitEnabled: (((String) -> Boolean))? = null, submit: () -> Unit) {
|
||||
PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
|
||||
close()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if (confirming) {
|
||||
SetPasswordView(
|
||||
generalGetString(R.string.confirm_passcode),
|
||||
generalGetString(R.string.confirm_verb),
|
||||
submitEnabled = { pwd -> pwd == enteredPassword }
|
||||
) {
|
||||
if (passcode.value == enteredPassword) {
|
||||
ksAppPassword.set(passcode.value)
|
||||
enteredPassword = ""
|
||||
passcode.value = ""
|
||||
close()
|
||||
submit()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) {
|
||||
enteredPassword = passcode.value
|
||||
passcode.value = ""
|
||||
confirming = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,4 @@ fun PreviewHowItWorks() {
|
||||
SimpleXTheme {
|
||||
HowItWorks(user = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,4 +56,3 @@ fun DeveloperView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -87,4 +87,4 @@ private fun HiddenProfileLayout(
|
||||
}
|
||||
SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+260
-6
@@ -7,22 +7,41 @@ import SectionTextFooter
|
||||
import SectionView
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimplexGreen
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.SetAppPasscodeView
|
||||
|
||||
enum class LAMode {
|
||||
SYSTEM,
|
||||
PASSCODE;
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
SYSTEM -> generalGetString(R.string.la_mode_system)
|
||||
PASSCODE -> generalGetString(R.string.la_mode_passcode)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrivacySettingsView(
|
||||
chatModel: ChatModel,
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -31,7 +50,7 @@ fun PrivacySettingsView(
|
||||
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
|
||||
AppBarTitle(stringResource(R.string.your_privacy))
|
||||
SectionView(stringResource(R.string.settings_section_title_device)) {
|
||||
ChatLockItem(chatModel.performLA, setPerformLA)
|
||||
ChatLockItem(chatModel, showSettingsModal, setPerformLA)
|
||||
SectionDivider()
|
||||
val context = LocalContext.current
|
||||
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
|
||||
@@ -52,10 +71,12 @@ fun PrivacySettingsView(
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SectionDivider()
|
||||
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
}) }
|
||||
SectionItemView {
|
||||
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
|
||||
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
|
||||
@@ -83,3 +104,236 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
private val laDelays = listOf(10, 30, 60, 180, 0)
|
||||
|
||||
@Composable
|
||||
fun SimplexLockView(
|
||||
chatModel: ChatModel,
|
||||
currentLAMode: SharedPreference<LAMode>,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
|
||||
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
|
||||
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
|
||||
val activity = LocalContext.current as FragmentActivity
|
||||
|
||||
fun resetLAEnabled(onOff: Boolean) {
|
||||
chatModel.controller.appPrefs.performLA.set(onOff)
|
||||
chatModel.performLA.value = onOff
|
||||
}
|
||||
|
||||
fun disableUnavailableLA() {
|
||||
resetLAEnabled(false)
|
||||
currentLAMode.set(LAMode.SYSTEM)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
|
||||
fun toggleLAMode(toLAMode: LAMode) {
|
||||
authenticate(
|
||||
if (toLAMode == LAMode.SYSTEM) {
|
||||
generalGetString(R.string.la_enter_app_passcode)
|
||||
} else {
|
||||
generalGetString(R.string.chat_lock)
|
||||
},
|
||||
generalGetString(R.string.change_lock_mode), activity
|
||||
) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> {
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
LAResult.Success -> {
|
||||
when (toLAMode) {
|
||||
LAMode.SYSTEM -> {
|
||||
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
currentLAMode.set(toLAMode)
|
||||
ksAppPassword.remove()
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Unavailable, is LAResult.Error -> {
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
currentLAMode.set(toLAMode)
|
||||
passcodeAlert(generalGetString(R.string.passcode_set))
|
||||
},
|
||||
cancel = {},
|
||||
close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is LAResult.Unavailable -> disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeLAPassword() {
|
||||
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
passcodeAlert(generalGetString(R.string.passcode_changed))
|
||||
}, cancel = {
|
||||
passcodeAlert(generalGetString(R.string.passcode_not_changed))
|
||||
}, close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is LAResult.Error -> laFailedAlert()
|
||||
is LAResult.Failed -> {}
|
||||
is LAResult.Unavailable -> disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.chat_lock))
|
||||
SectionView {
|
||||
EnableLock(performLA) { performLAToggle ->
|
||||
performLA.value = performLAToggle
|
||||
chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (performLAToggle) {
|
||||
when (currentLAMode.state.value) {
|
||||
LAMode.SYSTEM -> {
|
||||
setPerformLA(true, activity)
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
chatModel.controller.appPrefs.performLA.set(true)
|
||||
passcodeAlert(generalGetString(R.string.passcode_set))
|
||||
},
|
||||
cancel = {
|
||||
resetLAEnabled(false)
|
||||
}, close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPerformLA(false, activity)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
LockModeSelector(laMode) { newLAMode ->
|
||||
if (laMode.value == newLAMode) return@LockModeSelector
|
||||
if (chatModel.controller.appPrefs.performLA.get()) {
|
||||
toggleLAMode(newLAMode)
|
||||
} else {
|
||||
currentLAMode.set(newLAMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (performLA.value) {
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
|
||||
}
|
||||
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
|
||||
SectionDivider()
|
||||
SectionItemView({ changeLAPassword() }) {
|
||||
Text(generalGetString(R.string.la_change_app_passcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
stringResource(R.string.enable_lock), Modifier
|
||||
.padding(end = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Switch(
|
||||
checked = performLA.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockModeSelector(state: State<LAMode>, onSelected: (LAMode) -> Unit) {
|
||||
val values by remember { mutableStateOf(LAMode.values().map { it to it.text }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.lock_mode),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockDelaySelector(state: State<Int>, onSelected: (Int) -> Unit) {
|
||||
val delays = remember { if (laDelays.contains(state.value)) laDelays else listOf(state.value) + laDelays }
|
||||
val values by remember { mutableStateOf(delays.map { it to laDelayText(it) }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.lock_after),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
private fun laDelayText(t: Int): String {
|
||||
val m = t / 60
|
||||
val s = t % 60
|
||||
return if (t == 0) {
|
||||
generalGetString(R.string.la_immediately)
|
||||
} else if (m == 0 || s != 0) {
|
||||
// there are no options where both minutes and seconds are needed
|
||||
generalGetString(R.string.la_seconds).format(s)
|
||||
} else {
|
||||
generalGetString(R.string.la_minutes).format(m)
|
||||
}
|
||||
}
|
||||
|
||||
private fun passcodeAlert(title: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = generalGetString(R.string.la_please_remember_to_store_password)
|
||||
)
|
||||
}
|
||||
|
||||
+17
-17
@@ -42,7 +42,7 @@ import chat.simplex.app.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@@ -126,7 +126,7 @@ fun SettingsLayout(
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
userDisplayName: String,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
|
||||
@@ -174,7 +174,7 @@ fun SettingsLayout(
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
@@ -294,13 +294,20 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
|
||||
SectionItemView() {
|
||||
@Composable
|
||||
fun ChatLockItem(
|
||||
chatModel: ChatModel,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val currentLAMode = remember { chatModel.controller.appPrefs.laMode }
|
||||
SectionItemView(showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Outlined.Lock,
|
||||
if (performLA.value) Icons.Filled.Lock else Icons.Outlined.Lock,
|
||||
contentDescription = stringResource(R.string.chat_lock),
|
||||
tint = HighOrLowlight,
|
||||
tint = if (performLA.value) SimplexGreen else HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
@@ -309,14 +316,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Switch(
|
||||
checked = performLA.value,
|
||||
onCheckedChange = { setPerformLA(it) },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,7 +517,7 @@ private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
|
||||
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -538,7 +538,7 @@ fun PreviewSettingsLayout() {
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = {},
|
||||
setPerformLA = { _, _ -> },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showSettingsModalWithSearch = { },
|
||||
|
||||
+1
-1
@@ -388,4 +388,4 @@ private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference<Boolean>
|
||||
showMuteProfileAlert.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,20 @@
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled.</string>
|
||||
<string name="la_notice_turn_on">Turn on</string>
|
||||
<string name="la_lock_mode">SimpleX Lock mode</string>
|
||||
<string name="la_lock_mode_system">System authentication</string>
|
||||
<string name="la_lock_mode_passcode">Passcode entry</string>
|
||||
<string name="la_auth_failed">Authentication failed</string>
|
||||
<string name="la_could_not_be_verified">You could not be verified; please try again.</string>
|
||||
<string name="la_no_app_password">No app passcode</string>
|
||||
<string name="la_enter_app_passcode">Enter Passcode</string>
|
||||
<string name="la_current_app_passcode">Current Passcode</string>
|
||||
<string name="la_change_app_passcode">Change passcode</string>
|
||||
<string name="la_authenticate">Authenticate</string>
|
||||
<string name="la_immediately">Immediately</string>
|
||||
<string name="la_seconds">%d seconds</string>
|
||||
<string name="la_minutes">%d minutes</string>
|
||||
<string name="la_please_remember_to_store_password">Please remember or store it securely - there is no way to recover a lost password!</string>
|
||||
|
||||
<!-- LocalAuthentication.kt -->
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX Lock turned on</string>
|
||||
@@ -179,6 +193,8 @@
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="auth_open_chat_console">Open chat console</string>
|
||||
<string name="lock_not_enabled">SimpleX Lock not enabled!</string>
|
||||
<string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string>
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Message delivery error</string>
|
||||
@@ -735,6 +751,20 @@
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
<string name="full_backup">App data backup</string>
|
||||
<string name="enable_lock">Enable lock</string>
|
||||
<string name="lock_mode">Lock mode</string>
|
||||
<string name="lock_after">Lock after</string>
|
||||
<string name="submit_passcode">Submit</string>
|
||||
<string name="confirm_passcode">Confirm Passcode</string>
|
||||
<string name="incorrect_passcode">Incorrect passcode</string>
|
||||
<string name="new_passcode">New Passcode</string>
|
||||
<string name="authentication_cancelled">Authentication cancelled</string>
|
||||
<string name="la_mode_system">System</string>
|
||||
<string name="la_mode_passcode">Passcode</string>
|
||||
<string name="passcode_set">Passcode set!</string>
|
||||
<string name="passcode_changed">Passcode changed!</string>
|
||||
<string name="passcode_not_changed">Passcode not changed!</string>
|
||||
<string name="change_lock_mode">Change lock mode</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
|
||||
@@ -186,7 +186,7 @@ struct SimplexLockView: View {
|
||||
.onChange(of: laMode) { _ in
|
||||
if performLAModeReset {
|
||||
performLAModeReset = false
|
||||
} else if performLA {
|
||||
} else if prefPerformLA {
|
||||
toggleLAMode()
|
||||
} else {
|
||||
updateLAMode()
|
||||
|
||||
Reference in New Issue
Block a user