mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00
android, desktop: self destruct becomes better (#3598)
* android, desktop: self destruct becomes better * better way of doing it * fix script * firstOrNull * changes for review * comment --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
05b55d3fb5
commit
1438fd00e2
17 changed files with 188 additions and 88 deletions
|
@ -63,10 +63,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
|
||||
withBGApi {
|
||||
initChatController()
|
||||
runMigrations()
|
||||
}
|
||||
initChatControllerAndRunMigrations(false)
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ typedef long* chat_ctrl;
|
|||
*/
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_close_store(chat_ctrl ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
|
@ -93,6 +94,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j
|
|||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jclass clazz, jlong controller) {
|
||||
jstring res = (*env)->NewStringUTF(env, chat_close_store((void*)controller));
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
|
||||
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
|
||||
|
|
|
@ -30,6 +30,7 @@ typedef long* chat_ctrl;
|
|||
*/
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_close_store(chat_ctrl ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
|
@ -106,6 +107,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla
|
|||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass clazz, jlong controller) {
|
||||
jstring res = decode_to_utf8_string(env, chat_close_store((void*)controller));
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) {
|
||||
const char *_msg = encode_to_utf8_chars(env, msg);
|
||||
|
|
|
@ -103,13 +103,15 @@ fun MainScreen() {
|
|||
}
|
||||
|
||||
Box {
|
||||
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
|
||||
val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state }
|
||||
val localUserCreated = chatModel.localUserCreated.value
|
||||
var showInitializationView by remember { mutableStateOf(false) }
|
||||
when {
|
||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||
showChatDatabaseError -> {
|
||||
chatModel.chatDbStatus.value?.let {
|
||||
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
|
||||
if (!unauthorized.value && chatModel.chatDbStatus.value != null) {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +152,6 @@ fun MainScreen() {
|
|||
SwitchingUsersView()
|
||||
}
|
||||
|
||||
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
|
||||
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
|
||||
LaunchedEffect(Unit) {
|
||||
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
|
||||
|
|
|
@ -47,6 +47,7 @@ object ChatModel {
|
|||
val chatDbChanged = mutableStateOf<Boolean>(false)
|
||||
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||
val ctrlInitInProgress = mutableStateOf(false)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
|
|
|
@ -2145,7 +2145,15 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
|
|||
init {
|
||||
this.set = { value ->
|
||||
set(value)
|
||||
_state.value = value
|
||||
try {
|
||||
_state.value = value
|
||||
} catch (e: IllegalStateException) {
|
||||
// Can be `Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied`
|
||||
Log.i(TAG, e.stackTraceToString())
|
||||
withApi {
|
||||
_state.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
|
|||
// SimpleX API
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
|
||||
external fun chatCloseStore(ctrl: ChatCtrl): String
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
|
@ -35,57 +36,71 @@ val appPreferences: AppPreferences
|
|||
|
||||
val chatController: ChatController = ChatController
|
||||
|
||||
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
val ctrl = if (res is DBMigrationResult.OK) {
|
||||
migrated[1] as Long
|
||||
} else null
|
||||
chatController.ctrl = ctrl
|
||||
chatModel.chatDbEncrypted.value = dbKey != ""
|
||||
chatModel.chatDbStatus.value = res
|
||||
if (res != DBMigrationResult.OK) {
|
||||
Log.d(TAG, "Unable to migrate successfully: $res")
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
val user = chatController.apiGetActiveUser(null)
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = null
|
||||
chatModel.users.clear()
|
||||
if (appPlatform.isDesktop) {
|
||||
/**
|
||||
* Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start
|
||||
* because of default value of [OnboardingStage.OnboardingComplete]
|
||||
* */
|
||||
chatModel.localUserCreated.value = null
|
||||
if (chatController.listRemoteHosts()?.isEmpty() == true) {
|
||||
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
}
|
||||
chatController.startChatWithoutUser()
|
||||
} else {
|
||||
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
}
|
||||
} else {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() != newStage) {
|
||||
appPreferences.onboardingStage.set(newStage)
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
|
||||
chatModel.setDeliveryReceipts.value = true
|
||||
}
|
||||
chatController.startChat(user)
|
||||
platform.androidChatInitializedAndStarted()
|
||||
fun initChatControllerAndRunMigrations(ignoreSelfDestruct: Boolean) {
|
||||
if (ignoreSelfDestruct || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||
withBGApi {
|
||||
initChatController()
|
||||
runMigrations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
|
||||
try {
|
||||
chatModel.ctrlInitInProgress.value = true
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
val ctrl = if (res is DBMigrationResult.OK) {
|
||||
migrated[1] as Long
|
||||
} else null
|
||||
chatController.ctrl = ctrl
|
||||
chatModel.chatDbEncrypted.value = dbKey != ""
|
||||
chatModel.chatDbStatus.value = res
|
||||
if (res != DBMigrationResult.OK) {
|
||||
Log.d(TAG, "Unable to migrate successfully: $res")
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
val user = chatController.apiGetActiveUser(null)
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = null
|
||||
chatModel.users.clear()
|
||||
if (appPlatform.isDesktop) {
|
||||
/**
|
||||
* Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start
|
||||
* because of default value of [OnboardingStage.OnboardingComplete]
|
||||
* */
|
||||
chatModel.localUserCreated.value = null
|
||||
if (chatController.listRemoteHosts()?.isEmpty() == true) {
|
||||
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
}
|
||||
chatController.startChatWithoutUser()
|
||||
} else {
|
||||
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
}
|
||||
} else {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() != newStage) {
|
||||
appPreferences.onboardingStage.set(newStage)
|
||||
}
|
||||
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
|
||||
chatModel.setDeliveryReceipts.value = true
|
||||
}
|
||||
chatController.startChat(user)
|
||||
platform.androidChatInitializedAndStarted()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chatModel.ctrlInitInProgress.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,10 @@ fun UserPicker(
|
|||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
controller.reloadRemoteHosts()
|
||||
// Controller.ctrl can be null when self-destructing activates
|
||||
if (controller.ctrl != null && controller.ctrl != -1L) {
|
||||
controller.reloadRemoteHosts()
|
||||
}
|
||||
}
|
||||
val UsersView: @Composable ColumnScope.() -> Unit = {
|
||||
users.forEach { u ->
|
||||
|
|
|
@ -4,7 +4,6 @@ import SectionBottomSpacer
|
|||
import SectionDividerSpaced
|
||||
import SectionTextFooter
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -426,6 +425,7 @@ private fun authStopChat(m: ChatModel) {
|
|||
}
|
||||
is LAResult.Error -> {
|
||||
m.chatRunning.value = true
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Failed -> {
|
||||
m.chatRunning.value = true
|
||||
|
@ -459,6 +459,24 @@ suspend fun deleteChatAsync(m: ChatModel) {
|
|||
m.controller.apiDeleteStorage()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||
deleteChatDatabaseFiles()
|
||||
}
|
||||
|
||||
fun deleteChatDatabaseFiles() {
|
||||
val chat = File(dataDir, chatDatabaseFileName)
|
||||
val chatBak = File(dataDir, "$chatDatabaseFileName.bak")
|
||||
val agent = File(dataDir, agentDatabaseFileName)
|
||||
val agentBak = File(dataDir, "$agentDatabaseFileName.bak")
|
||||
chat.delete()
|
||||
chatBak.delete()
|
||||
agent.delete()
|
||||
agentBak.delete()
|
||||
filesDir.deleteRecursively()
|
||||
remoteHostsDir.deleteRecursively()
|
||||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
controller.appPrefs.storeDBPassphrase.set(true)
|
||||
}
|
||||
|
||||
private fun exportArchive(
|
||||
|
|
|
@ -17,7 +17,7 @@ object DatabaseUtils {
|
|||
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
||||
val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase)
|
||||
|
||||
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
||||
class KeyStoreItem(val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
||||
fun get(): String? {
|
||||
return cryptor.decryptData(
|
||||
passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null,
|
||||
|
@ -75,11 +75,11 @@ object DatabaseUtils {
|
|||
sealed class DBMigrationResult {
|
||||
@Serializable @SerialName("ok") object OK: DBMigrationResult()
|
||||
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
|
||||
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") data class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorMigration") data class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
|
||||
@Serializable @SerialName("errorSQL") data class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") data class Unknown(val json: String): DBMigrationResult()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,32 +1,45 @@
|
|||
package chat.simplex.common.views.localauth
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.database.deleteChatAsync
|
||||
import chat.simplex.common.views.database.stopChatAsync
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.Profile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.database.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
||||
val passcode = rememberSaveable { mutableStateOf("") }
|
||||
PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode),
|
||||
val allowToReact = rememberSaveable { mutableStateOf(true) }
|
||||
if (!allowToReact.value) {
|
||||
BackHandler {
|
||||
// do nothing until submit action finishes to prevent concurrent removing of storage
|
||||
}
|
||||
}
|
||||
PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode), buttonsEnabled = allowToReact,
|
||||
submit = {
|
||||
val sdPassword = ksSelfDestructPassword.get()
|
||||
if (sdPassword == passcode.value && authRequest.selfDestruct) {
|
||||
allowToReact.value = false
|
||||
deleteStorageAndRestart(m, sdPassword) { r ->
|
||||
authRequest.completed(r)
|
||||
}
|
||||
} else {
|
||||
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(MR.strings.incorrect_passcode))
|
||||
val r: LAResult = if (passcode.value == authRequest.password) {
|
||||
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
||||
initChatControllerAndRunMigrations(true)
|
||||
}
|
||||
LAResult.Success
|
||||
} else {
|
||||
LAResult.Error(generalGetString(MR.strings.incorrect_passcode))
|
||||
}
|
||||
authRequest.completed(r)
|
||||
}
|
||||
},
|
||||
|
@ -38,8 +51,28 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
|||
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
|
||||
withBGApi {
|
||||
try {
|
||||
stopChatAsync(m)
|
||||
deleteChatAsync(m)
|
||||
/** Waiting until [initChatController] finishes */
|
||||
while (m.ctrlInitInProgress.value) {
|
||||
delay(50)
|
||||
}
|
||||
if (m.chatRunning.value == true) {
|
||||
stopChatAsync(m)
|
||||
}
|
||||
val ctrl = m.controller.ctrl
|
||||
if (ctrl != null && ctrl != -1L) {
|
||||
/**
|
||||
* The following sequence can bring a user here:
|
||||
* the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code.
|
||||
* In this case database should be closed to prevent possible situation when OS can deny database removal command
|
||||
* */
|
||||
chatCloseStore(ctrl)
|
||||
}
|
||||
deleteChatDatabaseFiles()
|
||||
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
|
||||
m.chatId.value = null
|
||||
m.chatItems.clear()
|
||||
m.chats.clear()
|
||||
m.users.clear()
|
||||
ksAppPassword.set(password)
|
||||
ksSelfDestructPassword.remove()
|
||||
ntfManager.cancelAllNotifications()
|
||||
|
@ -67,13 +100,15 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
|||
m.currentUser.value = createdUser
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
if (createdUser != null) {
|
||||
controller.chatModel.chatRunning.value = false
|
||||
m.controller.startChat(createdUser)
|
||||
}
|
||||
ModalManager.fullscreen.closeModals()
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
AlertManager.shared.hideAllAlerts()
|
||||
AlertManager.privacySensitive.hideAllAlerts()
|
||||
completed(LAResult.Success)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to delete storage: ${e.stackTraceToString()}")
|
||||
completed(LAResult.Error(generalGetString(MR.strings.incorrect_passcode)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.chat.group.ProgressIndicator
|
||||
import chat.simplex.common.views.helpers.SimpleButton
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
|
@ -23,6 +24,7 @@ fun PasscodeView(
|
|||
reason: String? = null,
|
||||
submitLabel: String,
|
||||
submitEnabled: ((String) -> Boolean)? = null,
|
||||
buttonsEnabled: State<Boolean> = remember { mutableStateOf(true) },
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
|
@ -74,9 +76,9 @@ fun PasscodeView(
|
|||
}
|
||||
PasscodeEntry(passcode, true)
|
||||
Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel)
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,8 +119,8 @@ fun PasscodeView(
|
|||
Modifier.padding(start = 30.dp).height(s * 3),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel)
|
||||
SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel)
|
||||
SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -130,6 +132,9 @@ fun PasscodeView(
|
|||
} else {
|
||||
HorizontalLayout()
|
||||
}
|
||||
if (!buttonsEnabled.value) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
// Disallow to steal a focus by clicking on buttons or using Tab
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
|
@ -48,7 +49,9 @@ fun SetAppPasscodeView(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
SetPasswordView(title, generalGetString(MR.strings.save_verb)) {
|
||||
SetPasswordView(title, generalGetString(MR.strings.save_verb),
|
||||
// Do not allow to set app passcode == selfDestruct passcode
|
||||
submitEnabled = { pwd -> pwd != (if (passcodeKeychain.alias == ksSelfDestructPassword.alias) ksAppPassword else ksSelfDestructPassword).get() }) {
|
||||
enteredPassword = passcode.value
|
||||
passcode.value = ""
|
||||
confirming = true
|
||||
|
|
|
@ -429,6 +429,7 @@ fun SimplexLockView(
|
|||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
reason = generalGetString(MR.strings.la_app_passcode),
|
||||
submit = {
|
||||
passcodeAlert(generalGetString(MR.strings.passcode_changed))
|
||||
}, cancel = {
|
||||
|
@ -453,6 +454,7 @@ fun SimplexLockView(
|
|||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
passcodeKeychain = ksSelfDestructPassword,
|
||||
reason = generalGetString(MR.strings.self_destruct),
|
||||
submit = {
|
||||
selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed))
|
||||
}, cancel = {
|
||||
|
@ -553,7 +555,7 @@ fun SimplexLockView(
|
|||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(selfDestructDisplayName, "", ::isValidDisplayName)
|
||||
ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) })
|
||||
LaunchedEffect(selfDestructDisplayName.value) {
|
||||
val new = selfDestructDisplayName.value
|
||||
if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) {
|
||||
|
|
|
@ -919,6 +919,7 @@
|
|||
<string name="authentication_cancelled">Authentication cancelled</string>
|
||||
<string name="la_mode_system">System</string>
|
||||
<string name="la_mode_passcode">Passcode</string>
|
||||
<string name="la_app_passcode">App passcode</string>
|
||||
<string name="la_mode_off">Off</string>
|
||||
<string name="passcode_set">Passcode set!</string>
|
||||
<string name="passcode_changed">Passcode changed!</string>
|
||||
|
|
|
@ -2,8 +2,7 @@ package chat.simplex.common.platform
|
|||
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import java.util.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
|
@ -25,10 +24,7 @@ fun initApp() {
|
|||
override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications()
|
||||
}
|
||||
applyAppLocale()
|
||||
withBGApi {
|
||||
initChatController()
|
||||
runMigrations()
|
||||
}
|
||||
initChatControllerAndRunMigrations(false)
|
||||
// LALAL
|
||||
//testCrypto()
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ EXPORTS
|
|||
hs_init
|
||||
hs_init_with_rtsopts
|
||||
chat_migrate_init
|
||||
chat_close_store
|
||||
chat_send_cmd
|
||||
chat_send_remote_cmd
|
||||
chat_recv_msg
|
||||
|
|
Loading…
Add table
Reference in a new issue