diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt index 29dcc182..06fa6b2b 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/KeyboardParser.kt @@ -267,7 +267,13 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co if (!params.mId.mNumberRowEnabled && params.mId.mNumberRowInSymbols && params.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS) { // replace first symbols row with number row, but use the labels as popupKeys val numberRowCopy = numberRow.toMutableList() - numberRowCopy.forEachIndexed { index, keyData -> keyData.popup.symbol = baseKeys[0].getOrNull(index)?.label } + numberRowCopy.forEachIndexed { index, keyData -> + val symbolKey = baseKeys[0].getOrNull(index) ?: return@forEachIndexed + val symbols = mutableListOf() + symbolKey.label.takeIf { it.isNotEmpty() }?.let { symbols.add(it) } + symbolKey.popup.getPopupKeyLabels(params)?.let { symbols.addAll(it) } + if (symbols.isNotEmpty()) keyData.popup.symbols = symbols + } baseKeys[0] = numberRowCopy } else if (!params.mId.mNumberRowEnabled && params.mId.isAlphabetKeyboard && !hasBuiltInNumbers()) { if (baseKeys[0].any { it.popup.main != null || !it.popup.relevant.isNullOrEmpty() } // first row of baseKeys has any layout popup key @@ -290,7 +296,11 @@ class KeyboardParser(private val params: KeyboardParams, private val context: Co layout.forEachIndexed { i, row -> val baseRow = baseKeys.getOrNull(i) ?: return@forEachIndexed row.forEachIndexed { j, key -> - baseRow.getOrNull(j)?.popup?.symbol = key.label + val baseKey = baseRow.getOrNull(j) ?: return@forEachIndexed + val symbols = mutableListOf() + key.label.takeIf { it.isNotEmpty() }?.let { symbols.add(it) } + key.popup.getPopupKeyLabels(params)?.let { symbols.addAll(it) } + if (symbols.isNotEmpty()) baseKey.popup.symbols = symbols } } } diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/PopupSet.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/PopupSet.kt index 10b654b0..3f6c75b4 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/PopupSet.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/PopupSet.kt @@ -28,7 +28,7 @@ open class PopupSet( open fun isEmpty(): Boolean = main == null && relevant.isNullOrEmpty() var numberLabel: String? = null - var symbol: String? = null // maybe list of keys? + var symbols: Collection? = null fun merge(other: PopupSet?): PopupSet { if (other == null || other.isEmpty()) return this diff --git a/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt index 503dbf6f..75ca1ba4 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/PopupKeysUtils.kt @@ -31,7 +31,7 @@ fun createPopupKeysArray(popupSet: PopupSet<*>?, params: KeyboardParams, label: when (type) { POPUP_KEYS_NUMBER -> popupSet?.numberLabel?.let { popupKeys.add(it) } POPUP_KEYS_LAYOUT -> popupSet?.getPopupKeyLabels(params)?.let { popupKeys.addAll(it) } - POPUP_KEYS_SYMBOLS -> popupSet?.symbol?.let { popupKeys.add(it) } + POPUP_KEYS_SYMBOLS -> popupSet?.symbols?.let { popupKeys.addAll(it) } POPUP_KEYS_LANGUAGE -> params.mLocaleKeyboardInfos.getPopupKeys(label)?.let { popupKeys.addAll(it) } POPUP_KEYS_LANGUAGE_PRIORITY -> params.mLocaleKeyboardInfos.getPriorityPopupKeys(label)?.let { popupKeys.addAll(it) } } @@ -66,7 +66,7 @@ fun getHintLabel(popupSet: PopupSet<*>?, params: KeyboardParams, label: String): when (type) { POPUP_KEYS_NUMBER -> popupSet?.numberLabel?.let { hintLabel = it } POPUP_KEYS_LAYOUT -> popupSet?.getPopupKeyLabels(params)?.let { hintLabel = it.firstOrNull() } - POPUP_KEYS_SYMBOLS -> popupSet?.symbol?.let { hintLabel = it } + POPUP_KEYS_SYMBOLS -> popupSet?.symbols?.let { hintLabel = it.firstOrNull() } POPUP_KEYS_LANGUAGE -> params.mLocaleKeyboardInfos.getPopupKeys(label)?.let { hintLabel = it.firstOrNull() } POPUP_KEYS_LANGUAGE_PRIORITY -> params.mLocaleKeyboardInfos.getPriorityPopupKeys(label)?.let { hintLabel = it.firstOrNull() } } diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt index d2d76474..617a3c05 100644 --- a/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt +++ b/app/src/main/java/helium314/keyboard/settings/preferences/BackupRestorePreference.kt @@ -151,6 +151,14 @@ private fun backupLauncher(onError: (String) -> Unit): ManagedActivityResultLaun zipStream.putNextEntry(ZipEntry(PROTECTED_PREFS_FILE_NAME)) settingsToJsonStream(ctx.protectedPrefs().all, zipStream) zipStream.closeEntry() + // back up auxiliary SharedPreferences files used by individual features + // (gemini_prefs is intentionally excluded: it is EncryptedSharedPreferences + // whose values are tied to a device-specific master key and contains API keys) + for ((entryName, prefsForBackup) in auxiliaryPrefsToBackUp(ctx)) { + zipStream.putNextEntry(ZipEntry(entryName)) + settingsToJsonStream(prefsForBackup.all, zipStream) + zipStream.closeEntry() + } zipStream.close() } } catch (t: Throwable) { @@ -198,13 +206,20 @@ private fun restoreLauncher(onError: (String) -> Unit): ManagedActivityResultLau } else if (entry.name == PREFS_FILE_NAME) { val prefLines = String(zip.readBytes()).split("\n") val prefs = ctx.prefs() - prefs.edit { clear() } + prefs.edit(commit = true) { clear() } readJsonLinesToSettings(prefLines, prefs) } else if (entry.name == PROTECTED_PREFS_FILE_NAME) { val prefLines = String(zip.readBytes()).split("\n") val protectedPrefs = ctx.protectedPrefs() - protectedPrefs.edit { clear() } + protectedPrefs.edit(commit = true) { clear() } readJsonLinesToSettings(prefLines, protectedPrefs) + } else { + val auxPrefs = auxiliaryPrefsToBackUp(ctx)[entry.name] + if (auxPrefs != null) { + val prefLines = String(zip.readBytes()).split("\n") + auxPrefs.edit(commit = true) { clear() } + readJsonLinesToSettings(prefLines, auxPrefs) + } } zip.closeEntry() entry = zip.nextEntry @@ -274,13 +289,31 @@ private fun readJsonLinesToSettings(list: List, prefs: SharedPreferences "string set settings" -> Json.decodeFromString>>(i.next()).forEach { e.putStringSet(it.key, it.value) } } } - e.apply() + // commit synchronously so that post-restore actions and a possible process kill + // (e.g. the user closing the app immediately after restore) don't lose data + e.commit() return true } catch (e: Exception) { return false } } +/** + * Auxiliary SharedPreferences files (other than the main prefs and protectedPrefs) that + * should be included in backups. The key is the zip entry name to use, and the value + * is the SharedPreferences instance to read from / write back into on restore. + * + * NOTE: This must NOT include EncryptedSharedPreferences (e.g. "gemini_prefs"), because + * those values are encrypted with a device-bound master key and would be unreadable on + * any other device. Plus they typically hold credentials, which we don't want in a plain + * backup zip. + */ +private fun auxiliaryPrefsToBackUp(ctx: android.content.Context): Map = + mapOf( + FLOATING_KEYBOARD_PREFS_FILE_NAME + to DeviceProtectedUtils.getSharedPreferences(ctx, "floating_keyboard_prefs"), + ) + private fun restoreEntryToDir(zip: ZipInputStream, baseDir: File, entryName: String): Boolean { val file = File(baseDir, entryName) val canonicalBase = baseDir.canonicalFile @@ -294,6 +327,7 @@ private fun restoreEntryToDir(zip: ZipInputStream, baseDir: File, entryName: Str private const val PREFS_FILE_NAME = "preferences.json" private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json" +private const val FLOATING_KEYBOARD_PREFS_FILE_NAME = "floating_keyboard_preferences.json" private val backupFilePatterns by lazy { listOf( "blacklists${File.separator}.*\\.txt".toRegex(),