From 761a825a3de77a27a012ea3506ec3ff5e85e02b1 Mon Sep 17 00:00:00 2001 From: AsafMah Date: Mon, 18 May 2026 07:49:36 +0300 Subject: [PATCH 1/2] Fix: show all symbols in letter long-press popup When long-pressing a letter, the popup keyboard now includes all symbols defined at the corresponding position in the symbols layout, not just the primary label. Previously a symbols-layout entry such as `% per` or `( < { [` only contributed its first label to the letter's popup; the symbol key's own popup keys were discarded. addSymbolPopupKeys (and the number-row-in-symbols branch of addNumberRowOrPopupKeys) now collect both the symbol key's label and its popup labels via PopupSet.getPopupKeyLabels. PopupSet.symbol is renamed to PopupSet.symbols and changed from String? to Collection?. createPopupKeysArray addAll's them; getHintLabel keeps a single-character hint via firstOrNull(). Fixes #113 Assisted-by: GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/keyboard_parser/KeyboardParser.kt | 14 ++++++++++++-- .../internal/keyboard_parser/floris/PopupSet.kt | 2 +- .../keyboard/latin/utils/PopupKeysUtils.kt | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) 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 29dcc182a..06fa6b2b9 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 10b654b09..3f6c75b45 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 503dbf6f2..75ca1ba4b 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() } } From ec21ad0a2fc8c8620c8d36a987ea4de87c20d8d9 Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Mon, 18 May 2026 07:46:08 +0300 Subject: [PATCH 2/2] fix: include floating keyboard prefs in backup/restore, use synchronous commit The settings backup/restore previously only included the main SharedPreferences (device-protected) and the "protected" SharedPreferences, missing the separate loating_keyboard_prefs file used by FloatingKeyboardManager. After import on a fresh install, that file would stay empty and the floating-keyboard window position would silently reset. Now also back up and restore loating_keyboard_prefs via a new loating_keyboard_preferences.json zip entry. Encrypted gemini_prefs (EncryptedSharedPreferences holding device-bound API keys) is intentionally excluded, both because the master key is device-specific and because we don't want plain backups of credentials. Also switch the SharedPreferences writes during restore from pply() to commit(). The post-restore code path runs `AppUpgrade.checkVersionUpgrade`, `Settings.startListener()`, etc. immediately afterwards, and can also be terminated by the user closing the activity. `commit()` guarantees the new preferences are persisted before any of that happens. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../preferences/BackupRestorePreference.kt | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) 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 d2d764743..617a3c053 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(),