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:
Stanislav Dmitrenko 2023-12-30 00:47:25 +07:00 committed by GitHub
parent 05b55d3fb5
commit 1438fd00e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 188 additions and 88 deletions

View file

@ -63,10 +63,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
tmpDir.deleteRecursively()
tmpDir.mkdir()
withBGApi {
initChatController()
runMigrations()
}
initChatControllerAndRunMigrations(false)
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
}

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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>()

View file

@ -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
}
}
}
}
}

View file

@ -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
}
}

View file

@ -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 ->

View file

@ -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(

View file

@ -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()
}

View file

@ -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)))
}
}

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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>

View file

@ -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()
}

View file

@ -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