Files
openless/openless-all/app/android/kotlin/OpenLessAndroidPreferences.kt
16c3f841b5 chore(release): prepare 2.0.0-Beta.2 build 20260924
Integrate the reviewed 2.0 PRs and local fixes from 03136853.
Preserve the verified subtitle behavior and model/runtime corrections.
Keep the numeric Beta tag compatible with released clients while exposing
the date as application build metadata.

Co-authored-by: DepengWang <2818245+DepengWang@users.noreply.github.com>
Co-authored-by: HKLHaoBin <we3q@qq.com>
Co-authored-by: Nahida <1139500183@qq.com>
Co-authored-by: TRIP <1933142963@qq.com>
Co-authored-by: chyuwei <weichangyuwcy@gmail.com>
Co-authored-by: leonoxo <leonoxo@gmail.com>
Co-authored-by: tayir <arnoldincredible@126.com>
2026-09-24 17:28:24 +08:00

167 lines
6.8 KiB
Kotlin

package com.openless.app
import android.content.Context
import android.util.Log
import java.io.File
import org.json.JSONObject
/** Reads Android-visible preferences without depending on the Rust coordinator. */
object OpenLessAndroidPreferences {
private const val TAG = "OpenLessAndroidPrefs"
private const val APP_DIR = "OpenLess"
private const val PREFERENCES_FILE = "preferences.json"
private const val KEY_OVERLAY_TRIGGER = "androidOverlayTrigger"
private const val KEY_OVERLAY_ACTIVATION_MODE = "androidOverlayActivationMode"
private const val KEY_OVERLAY_LEFT_SWIPE_ACTION = "androidOverlayLeftSwipeAction"
private const val KEY_OVERLAY_CANCEL_SWIPE_DIRECTION = "androidOverlayCancelSwipeDirection"
private const val KEY_OVERLAY_GESTURE_ACTIONS = "androidOverlayGestureActions"
private const val KEY_OVERLAY_SIZE_DP = "androidOverlaySizeDp"
private const val KEY_CHINESE_SCRIPT_PREFERENCE = "chineseScriptPreference"
private const val DEFAULT_OVERLAY_SIZE_DP = 72
private const val MIN_OVERLAY_SIZE_DP = 48
private const val MAX_OVERLAY_SIZE_DP = 120
private val VALID_OVERLAY_TRIGGERS = setOf("background", "always")
private val VALID_OVERLAY_ACTIVATION_MODES = setOf("tap", "long_press")
private val VALID_OVERLAY_LEFT_SWIPE_ACTIONS = setOf("translation", "style_pack")
private val VALID_OVERLAY_CANCEL_SWIPE_DIRECTIONS = setOf("up", "down")
private val VALID_OVERLAY_GESTURE_ACTIONS =
setOf("none", "quick_note", "translation", "style_pack", "cancel", "qa")
fun overlayTriggerMode(context: Context): String? {
val value = readPreferenceString(context, KEY_OVERLAY_TRIGGER) ?: return null
if (value == "keyboard") {
return "background"
}
return value.takeIf { it in VALID_OVERLAY_TRIGGERS }
}
/** True when preferences still store legacy `"keyboard"` (before migration). */
fun isKeyboardOverlayTrigger(context: Context): Boolean {
return readPreferenceString(context, KEY_OVERLAY_TRIGGER) == "keyboard"
}
fun overlayActivationMode(context: Context): String {
return readPreferenceString(context, KEY_OVERLAY_ACTIVATION_MODE)?.takeIf {
it in VALID_OVERLAY_ACTIVATION_MODES
} ?: "tap"
}
fun overlayLeftSwipeAction(context: Context): String {
return readPreferenceString(context, KEY_OVERLAY_LEFT_SWIPE_ACTION)?.takeIf {
it in VALID_OVERLAY_LEFT_SWIPE_ACTIONS
} ?: "translation"
}
fun overlayCancelSwipeDirection(context: Context): String {
return readPreferenceString(context, KEY_OVERLAY_CANCEL_SWIPE_DIRECTION)?.takeIf {
it in VALID_OVERLAY_CANCEL_SWIPE_DIRECTIONS
} ?: "up"
}
fun overlayGestureAction(context: Context, direction: String): String {
for (file in preferenceFiles(context).distinctBy { it.absolutePath }) {
if (!file.isFile) continue
try {
val actions = JSONObject(file.readText()).optJSONObject(KEY_OVERLAY_GESTURE_ACTIONS)
val value = actions?.optString(direction, "")?.takeIf {
it in VALID_OVERLAY_GESTURE_ACTIONS
}
if (value != null) return value
} catch (error: Throwable) {
Log.w(TAG, "read gesture actions ${file.absolutePath} failed", error)
}
}
val legacyLeft = overlayLeftSwipeAction(context)
val legacyCancel = overlayCancelSwipeDirection(context)
return when (direction) {
"up" -> if (legacyCancel == "up") "cancel" else "none"
"down" -> if (legacyCancel == "down") "cancel" else "none"
"left" -> legacyLeft
"right" -> "qa"
else -> "none"
}
}
fun overlaySizeDp(context: Context): Int {
return readPreferenceInt(context, KEY_OVERLAY_SIZE_DP)
?.coerceIn(MIN_OVERLAY_SIZE_DP, MAX_OVERLAY_SIZE_DP) ?: DEFAULT_OVERLAY_SIZE_DP
}
fun chineseScriptPreference(context: Context): String {
return readPreferenceString(context, KEY_CHINESE_SCRIPT_PREFERENCE)
?.takeIf { it == "simplified" || it == "traditional" }
?: "simplified"
}
fun strokeAssociationEnabled(context: Context): Boolean =
readPreferenceBoolean(context, "strokeAssociationEnabled") ?: true
fun strokeUsageEnabled(context: Context): Boolean =
readPreferenceBoolean(context, "strokeUsageEnabled") ?: true
private fun readPreferenceString(context: Context, key: String): String? {
for (file in preferenceFiles(context).distinctBy { it.absolutePath }) {
if (!file.isFile) {
continue
}
val value =
try {
JSONObject(file.readText()).optString(key, "")
} catch (error: Throwable) {
Log.w(TAG, "read ${file.absolutePath} failed", error)
""
}
if (value.isNotBlank()) {
return value
}
}
return null
}
private fun readPreferenceInt(context: Context, key: String): Int? {
for (file in preferenceFiles(context).distinctBy { it.absolutePath }) {
if (!file.isFile) {
continue
}
val value =
try {
val json = JSONObject(file.readText())
if (json.has(key)) json.optInt(key) else null
} catch (error: Throwable) {
Log.w(TAG, "read ${file.absolutePath} failed", error)
null
}
if (value != null) {
return value
}
}
return null
}
private fun readPreferenceBoolean(context: Context, key: String): Boolean? {
for (file in preferenceFiles(context).distinctBy { it.absolutePath }) {
if (!file.isFile) continue
try {
val json = JSONObject(file.readText())
if (json.has(key)) return json.optBoolean(key)
} catch (error: Throwable) {
Log.w(TAG, "read ${file.absolutePath} failed", error)
}
}
return null
}
private fun preferenceFiles(context: Context): List<File> {
val files = mutableListOf<File>()
// Prefer app-private filesDir (Rust data_dir); never rely on /data/local/tmp.
files += File(File(context.filesDir, APP_DIR), PREFERENCES_FILE)
val envDir = System.getenv("TAURI_ANDROID_APP_DATA_DIR")
if (!envDir.isNullOrBlank()) {
files += File(File(envDir), APP_DIR).resolve(PREFERENCES_FILE)
}
// Legacy probe only — some older builds may have written under cacheDir.
files += File(File(context.cacheDir, APP_DIR), PREFERENCES_FILE)
return files
}
}