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>
167 lines
6.8 KiB
Kotlin
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
|
|
}
|
|
}
|