From e5acfb272d71cde97e8be09d517624498ede561f Mon Sep 17 00:00:00 2001 From: WhiredPlanck Date: Mon, 7 Oct 2024 10:05:21 +0800 Subject: [PATCH 1/4] refactors: always pass the copy of the active theme to the views ... ... and related managers, therefore the stuffs in the scope can always get the initialized and unique Theme instance. --- .../osfans/trime/data/theme/ColorManager.kt | 15 +- .../osfans/trime/data/theme/EventManager.kt | 2 +- .../osfans/trime/data/theme/FontManager.kt | 11 +- .../osfans/trime/data/theme/ThemeManager.kt | 13 +- .../trime/ime/composition/Composition.kt | 8 +- .../ime/composition/CompositionPopupWindow.kt | 43 +++--- .../com/osfans/trime/ime/core/InputView.kt | 17 +-- .../trime/ime/core/TrimeInputMethodService.kt | 15 +- .../com/osfans/trime/ime/keyboard/Keyboard.kt | 13 +- .../osfans/trime/ime/keyboard/KeyboardView.kt | 144 +++++++++--------- .../trime/ime/keyboard/KeyboardWindow.kt | 10 +- .../com/osfans/trime/ime/symbol/TabManager.kt | 13 +- .../trime/ui/fragments/KeyboardFragment.kt | 3 +- app/src/main/res/layout/composition_root.xml | 20 --- .../res/layout/keyboard_popup_keyboard.xml | 21 --- 15 files changed, 151 insertions(+), 197 deletions(-) delete mode 100644 app/src/main/res/layout/composition_root.xml delete mode 100644 app/src/main/res/layout/keyboard_popup_keyboard.xml diff --git a/app/src/main/java/com/osfans/trime/data/theme/ColorManager.kt b/app/src/main/java/com/osfans/trime/data/theme/ColorManager.kt index 46b42ba228..08db7013cf 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/ColorManager.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/ColorManager.kt @@ -23,7 +23,7 @@ import timber.log.Timber import java.io.File object ColorManager { - private val theme get() = ThemeManager.activeTheme + private lateinit var theme: Theme private val prefs = AppPrefs.defaultInstance().theme private val backgroundFolder get() = theme.generalStyle.backgroundFolder @@ -81,7 +81,7 @@ object ColorManager { } ?: mapOf() fun interface OnColorChangeListener { - fun onColorChange() + fun onColorChange(theme: Theme) } private val onChangeListeners = WeakHashSet() @@ -95,16 +95,11 @@ object ColorManager { } private fun fireChange() { - onChangeListeners.forEach { it.onColorChange() } + onChangeListeners.forEach { it.onColorChange(theme) } } fun init(configuration: Configuration) { isNightMode = configuration.isNightMode() - runCatching { - ThemeManager.init() - }.getOrElse { - Timber.e(it, "Setting up theme failed!") - } } fun onSystemNightModeChange(isNight: Boolean) { @@ -115,10 +110,12 @@ object ColorManager { } /** 每次切换主题后,都要调用此函数,初始化配色 */ - fun refresh() { + fun resetCache(theme: Theme) { lastDarkColorSchemeId = null lastLightColorSchemeId = null + this.theme = theme + val selected = prefs.selectedColor val fromStyle = theme.generalStyle.colorScheme val default = "default" // 主題中的 default 配色 diff --git a/app/src/main/java/com/osfans/trime/data/theme/EventManager.kt b/app/src/main/java/com/osfans/trime/data/theme/EventManager.kt index e131859c1c..167ecf0169 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/EventManager.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/EventManager.kt @@ -20,5 +20,5 @@ object EventManager { return event } - fun refresh() = eventCache.clear() + fun resetCache() = eventCache.clear() } diff --git a/app/src/main/java/com/osfans/trime/data/theme/FontManager.kt b/app/src/main/java/com/osfans/trime/data/theme/FontManager.kt index b8acb17fac..57f0255632 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/FontManager.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/FontManager.kt @@ -14,6 +14,8 @@ import timber.log.Timber import java.io.File object FontManager { + private lateinit var theme: Theme + private enum class FontKey { HANB_FONT, LATIN_FONT, @@ -28,16 +30,17 @@ object FontManager { } private val fontDir get() = File(DataManager.userDataDir, "fonts") - var hanBFont: Typeface = getTypefaceOrDefault(FontKey.HANB_FONT.name) + lateinit var hanBFont: Typeface private set - var latinFont: Typeface = getTypefaceOrDefault(FontKey.LATIN_FONT.name) + lateinit var latinFont: Typeface private set private val typefaceCache = mutableMapOf() private val fontFamilyCache = mutableMapOf() - fun refresh() { + fun resetCache(theme: Theme) { typefaceCache.clear() fontFamilyCache.clear() + this.theme = theme hanBFont = getTypefaceOrDefault(FontKey.HANB_FONT.name) latinFont = getTypefaceOrDefault(FontKey.LATIN_FONT.name) } @@ -96,7 +99,7 @@ object FontManager { } private fun getFontFromStyle(key: String): List? { - val style = ThemeManager.activeTheme.generalStyle + val style = theme.generalStyle return when (FontKey.entries.firstOrNull { it.name == key.uppercase() }) { FontKey.HANB_FONT -> style.hanbFont FontKey.LATIN_FONT -> style.latinFont diff --git a/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt b/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt index 77fcdb79ac..4d972b780d 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt @@ -47,9 +47,9 @@ object ThemeManager { userThemes.clear() sharedThemes.addAll(listThemes(DataManager.sharedDataDir)) userThemes.addAll(listThemes(DataManager.userDataDir)) + setNormalTheme(prefs.selectedTheme) } - // 在初始化 ColorManager 时会被赋值 lateinit var activeTheme: Theme private set @@ -60,14 +60,13 @@ object ThemeManager { fun setNormalTheme(name: String) { Theme(name).let { if (::activeTheme.isInitialized) { - if (it == activeTheme) return + if (activeTheme == it) return } + EventManager.resetCache() + FontManager.resetCache(it) + ColorManager.resetCache(it) + TabManager.resetCache(it) activeTheme = it - // 由于这里的顺序不能打乱,不适合使用 listener - EventManager.refresh() - FontManager.refresh() - ColorManager.refresh() - TabManager.refresh() } } } diff --git a/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt b/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt index 77288a1e03..286c0e35e1 100644 --- a/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt +++ b/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt @@ -19,7 +19,6 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.ScaleXSpan import android.text.style.UnderlineSpan -import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration @@ -31,7 +30,7 @@ import com.osfans.trime.core.RimeProto import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.EventManager import com.osfans.trime.data.theme.FontManager -import com.osfans.trime.data.theme.ThemeManager +import com.osfans.trime.data.theme.Theme import com.osfans.trime.data.theme.model.CompositionComponent import com.osfans.trime.ime.core.TrimeInputMethodService import com.osfans.trime.ime.keyboard.Event @@ -46,10 +45,9 @@ import kotlin.math.absoluteValue @SuppressLint("AppCompatCustomView") class Composition( context: Context, - attrs: AttributeSet?, -) : TextView(context, attrs) { + private val theme: Theme, +) : TextView(context) { private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop - private val theme = ThemeManager.activeTheme private val keyTextSize = theme.generalStyle.keyTextSize private val labelTextSize = theme.generalStyle.labelTextSize diff --git a/app/src/main/java/com/osfans/trime/ime/composition/CompositionPopupWindow.kt b/app/src/main/java/com/osfans/trime/ime/composition/CompositionPopupWindow.kt index 5547a85481..bde9353270 100644 --- a/app/src/main/java/com/osfans/trime/ime/composition/CompositionPopupWindow.kt +++ b/app/src/main/java/com/osfans/trime/ime/composition/CompositionPopupWindow.kt @@ -12,7 +12,6 @@ import android.os.Build.VERSION_CODES import android.os.Handler import android.os.Looper import android.view.Gravity -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -25,8 +24,6 @@ import com.osfans.trime.daemon.launchOnReady import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.Theme -import com.osfans.trime.data.theme.ThemeManager -import com.osfans.trime.databinding.CompositionRootBinding import com.osfans.trime.ime.bar.QuickBar import com.osfans.trime.ime.broadcast.InputBroadcastReceiver import com.osfans.trime.ime.dependency.InputScope @@ -34,6 +31,10 @@ import com.osfans.trime.ime.enums.PopupPosition import com.osfans.trime.ime.keyboard.CommonKeyboardActionListener import me.tatarka.inject.annotations.Inject import splitties.dimensions.dp +import splitties.views.dsl.core.add +import splitties.views.dsl.core.horizontalLayout +import splitties.views.dsl.core.lParams +import splitties.views.dsl.core.wrapContent import timber.log.Timber @InputScope @@ -50,18 +51,22 @@ class CompositionPopupWindow( AppPrefs.defaultInstance().keyboard.popupWindowEnabled && theme.generalStyle.window.isNotEmpty() - val binding = - CompositionRootBinding.inflate(LayoutInflater.from(ctx)).apply { - root.visibility = if (isPopupWindowEnabled) View.VISIBLE else View.GONE - composition.run { - setOnActionMoveListener { x, y -> - updatePopupWindow(x.toInt(), y.toInt()) - } - setOnSelectCandidateListener { idx -> - rime.launchOnReady { it.selectCandidate(idx) } - } - setKeyboardActionListener(commonKeyboardActionListener.listener) + val composition = + Composition(ctx, theme).apply { + setOnActionMoveListener { x, y -> + updatePopupWindow(x.toInt(), y.toInt()) + } + setOnSelectCandidateListener { idx -> + rime.launchOnReady { it.selectCandidate(idx) } } + setKeyboardActionListener(commonKeyboardActionListener.listener) + } + + val root = + ctx.horizontalLayout { + layoutParams = ViewGroup.LayoutParams(wrapContent, wrapContent) + visibility = if (isPopupWindowEnabled) View.VISIBLE else View.GONE + add(composition, lParams(wrapContent, wrapContent)) } // 悬浮窗口是否可移動 @@ -80,7 +85,7 @@ class CompositionPopupWindow( private var popupWindowPos = PopupPosition.fromString(theme.generalStyle.layout.position) private val mPopupWindow = - PopupWindow(binding.root).apply { + PopupWindow(root).apply { isClippingEnabled = false inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { @@ -219,14 +224,14 @@ class CompositionPopupWindow( private fun updateCompositionView() { if (isPopupWindowMovable == "once") { - popupWindowPos = PopupPosition.fromString(ThemeManager.activeTheme.generalStyle.layout.position) + popupWindowPos = PopupPosition.fromString(theme.generalStyle.layout.position) } - binding.root.measure( + root.measure( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) - mPopupWindow.width = binding.root.measuredWidth - mPopupWindow.height = binding.root.measuredHeight + mPopupWindow.width = root.measuredWidth + mPopupWindow.height = root.measuredHeight mPopupHandler.post(mPopupTimer) } diff --git a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt index 5c2fa7b576..3376705fb2 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt @@ -28,7 +28,7 @@ import com.osfans.trime.core.RimeResponse import com.osfans.trime.daemon.RimeSession import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.data.theme.ColorManager -import com.osfans.trime.data.theme.ThemeManager +import com.osfans.trime.data.theme.Theme import com.osfans.trime.ime.bar.QuickBar import com.osfans.trime.ime.candidates.CompactCandidateModule import com.osfans.trime.ime.composition.CompositionPopupWindow @@ -67,10 +67,10 @@ import splitties.views.imageDrawable */ @SuppressLint("ViewConstructor") class InputView( - val service: TrimeInputMethodService, - val rime: RimeSession, + private val service: TrimeInputMethodService, + private val rime: RimeSession, + private val theme: Theme, ) : ConstraintLayout(service) { - private val theme get() = ThemeManager.activeTheme private var shouldUpdateNavbarForeground = false private var shouldUpdateNavbarBackground = false private val navbarBackground get() = AppPrefs.defaultInstance().theme.navbarBackground @@ -83,17 +83,17 @@ class InputView( private val leftPaddingSpace = view(::View) { - setOnClickListener { placeholderListener } + setOnClickListener(placeholderListener) } private val rightPaddingSpace = view(::View) { - setOnClickListener { placeholderListener } + setOnClickListener(placeholderListener) } private val bottomPaddingSpace = view(::View) { - setOnClickListener { placeholderListener } + setOnClickListener(placeholderListener) } private val notificationHandlerJob: Job @@ -353,7 +353,7 @@ class InputView( val previous = ctx.menu.run { pageSize * pageNumber } val highlightedIdx = ctx.menu.highlightedCandidateIndex if (composition.isPopupWindowEnabled) { - val sticky = composition.binding.composition.update(ctx) + val sticky = composition.composition.update(ctx) compactCandidate.adapter.updateCandidates(candidates, isLastPage, previous, highlightedIdx, sticky) } else { compactCandidate.adapter.updateCandidates(candidates, isLastPage, previous, highlightedIdx) @@ -376,7 +376,6 @@ class InputView( false }.also { composition.isCursorUpdated = it } } - keyboardWindow.mainKeyboardView.invalidateComposingKeys() } fun updateSelection( diff --git a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt index 3073be602b..914dd99478 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt @@ -49,6 +49,7 @@ import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.data.schema.SchemaManager import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.EventManager +import com.osfans.trime.data.theme.Theme import com.osfans.trime.data.theme.ThemeManager import com.osfans.trime.ime.broadcast.IntentReceiver import com.osfans.trime.ime.enums.FullscreenMode @@ -105,7 +106,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { private val onColorChangeListener = ColorManager.OnColorChangeListener { lifecycleScope.launch(Dispatchers.Main) { - recreateInputView() + recreateInputView(it) } } @@ -221,6 +222,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { handleRimeResponse(it) } } + ColorManager.addOnChangedListener(onColorChangeListener) super.onCreate() instance = this // MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops @@ -229,13 +231,13 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { // could crash // and lead to a crash loop Timber.d("onCreate") - ColorManager.addOnChangedListener(onColorChangeListener) mIntentReceiver = IntentReceiver().also { it.registerReceiver(this) } postRimeJob { ColorManager.init(resources.configuration) + ThemeManager.init() InputFeedbackManager.init() restartSystemStartTimingSync() shouldUpdateRimeOption = true @@ -265,7 +267,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { private fun handleRimeNotification(notification: RimeNotification<*>) { if (notification is RimeNotification.SchemaNotification) { SchemaManager.init(notification.value.id) - recreateInputView() + recreateInputView(ThemeManager.activeTheme) } else if (notification is RimeNotification.OptionNotification) { val value = notification.value.value when (val option = notification.value.option) { @@ -310,11 +312,10 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { /** Must be called on the UI thread * * 重置鍵盤、候選條、狀態欄等 !!注意,如果其中調用Rime.setOption,切換方案會卡住 */ - fun recreateInputView() { - inputView = InputView(this, rime) + fun recreateInputView(theme: Theme) { shouldUpdateRimeOption = true // 不能在Rime.onMessage中調用set_option,會卡死 updateComposing() // 切換主題時刷新候選 - setInputView(inputView!!) + setInputView(InputView(this, rime, theme).also { inputView = it }) initializationUi = null } @@ -413,7 +414,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { override fun onCreateInputView(): View { Timber.d("onCreateInputView") postRimeJob(Dispatchers.Main) { - recreateInputView() + recreateInputView(ThemeManager.activeTheme) } initializationUi = InitializationUi(this) return initializationUi!!.root diff --git a/app/src/main/java/com/osfans/trime/ime/keyboard/Keyboard.kt b/app/src/main/java/com/osfans/trime/ime/keyboard/Keyboard.kt index 8923db9ba5..84dea6575d 100644 --- a/app/src/main/java/com/osfans/trime/ime/keyboard/Keyboard.kt +++ b/app/src/main/java/com/osfans/trime/ime/keyboard/Keyboard.kt @@ -8,7 +8,6 @@ import android.view.KeyEvent import com.osfans.trime.data.prefs.AppPrefs.Companion.defaultInstance import com.osfans.trime.data.theme.EventManager import com.osfans.trime.data.theme.Theme -import com.osfans.trime.data.theme.ThemeManager import com.osfans.trime.ime.keyboard.KeyboardPrefs.isLandscapeMode import com.osfans.trime.util.CollectionUtils.obtainBoolean import com.osfans.trime.util.CollectionUtils.obtainFloat @@ -23,7 +22,9 @@ import kotlin.math.abs /** 從YAML中加載鍵盤配置,包含多個[按鍵][Key]。 */ @Suppress("ktlint:standard:property-naming") -class Keyboard() { +class Keyboard( + private val theme: Theme, +) { /** 按鍵默認水平間距 */ private var horizontalGap: Int @@ -114,7 +115,7 @@ class Keyboard() { * keyboard will fit as many keys as possible in each row. * @param horizontalPadding 按鍵水平間距 */ - constructor(characters: CharSequence, columns: Int, horizontalPadding: Int) : this() { + constructor(theme: Theme, characters: CharSequence, columns: Int, horizontalPadding: Int) : this(theme) { var x = 0 var y = 0 var column = 0 @@ -144,7 +145,6 @@ class Keyboard() { } private fun getKeyboardConfig(name: String): ConfigMap? { - val theme = ThemeManager.activeTheme val keyboardConfig = theme.keyboards.getMap(name) ?: theme.keyboards.getMap("default") @@ -155,8 +155,8 @@ class Keyboard() { return keyboardConfig } - constructor(name: String) : this() { - val theme = ThemeManager.activeTheme + constructor(theme: Theme, name: String) : this(theme) { + val keyboardConfig = getKeyboardConfig(name) mLabelTransform = obtainString(keyboardConfig, "label_transform", "none") @@ -648,7 +648,6 @@ class Keyboard() { init { // 橫屏模式下,键盘左右两侧到屏幕边缘的距离 - val theme = ThemeManager.activeTheme val keyboardSidePadding = theme.generalStyle.keyboardPadding val keyboardSidePaddingLandscape = theme.generalStyle.keyboardPaddingLand diff --git a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardView.kt b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardView.kt index dc6806170a..95930ce9cb 100644 --- a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardView.kt +++ b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardView.kt @@ -4,6 +4,7 @@ package com.osfans.trime.ime.keyboard +import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList import android.graphics.Bitmap @@ -21,20 +22,23 @@ import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.PopupWindow import com.osfans.trime.core.Rime import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.FontManager -import com.osfans.trime.data.theme.ThemeManager -import com.osfans.trime.databinding.KeyboardPopupKeyboardBinding +import com.osfans.trime.data.theme.Theme import com.osfans.trime.ime.enums.KeyEventType import com.osfans.trime.util.LeakGuardHandlerWrapper import com.osfans.trime.util.indexOfStateSet import com.osfans.trime.util.sp import com.osfans.trime.util.stateDrawableAt import splitties.dimensions.dp -import splitties.systemservices.layoutInflater +import splitties.views.dsl.core.add +import splitties.views.dsl.core.horizontalLayout +import splitties.views.dsl.core.lParams +import splitties.views.dsl.core.matchParent import splitties.views.dsl.core.textView import splitties.views.dsl.core.wrapContent import splitties.views.gravityBottomCenter @@ -46,12 +50,12 @@ import kotlin.math.min import kotlin.math.pow /** 顯示[鍵盤][Keyboard]及[按鍵][Key] */ +@SuppressLint("ViewConstructor") class KeyboardView( context: Context, + private val theme: Theme, ) : View(context), View.OnClickListener { - private val theme get() = ThemeManager.activeTheme - private var mKeyboard: Keyboard? = null private var mCurrentKeyIndex = NOT_A_KEY private val mKeyTextSize = theme.generalStyle.keyTextSize.toFloat() @@ -427,18 +431,17 @@ class KeyboardView( removeMessages() mRepeatKeyIndex = NOT_A_KEY mKeyboard = keyboard - val keys = mKeyboard!!.keys - mKeys = keys.toTypedArray() + mKeys = keyboard!!.keys.toTypedArray() setKeyboardBackground() - requestLayout() // Hint to reallocate the buffer if the size changed mKeyboardChanged = true - invalidateAllKeys() computeProximityThreshold(keyboard) mMiniKeyboardCache.clear() // Not really necessary to do every time, but will free up views // Switching to a different keyboard should abort any pending keys so that the key up // doesn't get delivered to the old or new keyboard mAbortKey = true // Until the next ACTION_DOWN + invalidateAllKeys() + requestLayout() } /** @@ -1080,77 +1083,72 @@ class KeyboardView( return false } - var mMiniKeyboardContainer = mMiniKeyboardCache[popupKey] - val mMiniKeyboard: KeyboardView - if (mMiniKeyboardContainer == null) { - mMiniKeyboardContainer = KeyboardPopupKeyboardBinding.inflate(layoutInflater).root - mMiniKeyboard = mMiniKeyboardContainer.findViewById(android.R.id.keyboardView) - val closeButton = mMiniKeyboardContainer.findViewById(android.R.id.closeButton) - closeButton?.setOnClickListener(this) - mMiniKeyboard.keyboardActionListener = - object : KeyboardActionListener { - override fun onEvent(event: Event) { - keyboardActionListener?.onEvent(event) - dismissPopupKeyboard() - } - - override fun onKey( - keyEventCode: Int, - metaState: Int, - ) { - keyboardActionListener?.onKey(keyEventCode, metaState) - dismissPopupKeyboard() - } - - override fun onText(text: CharSequence) { - keyboardActionListener?.onText(text) - dismissPopupKeyboard() - } + val miniKeyboardLayout = + mMiniKeyboardCache[popupKey] ?: horizontalLayout { + layoutParams = ViewGroup.LayoutParams(matchParent, wrapContent) + }.also { mMiniKeyboardCache[popupKey] = it } + + val miniKeyboardView = + miniKeyboardLayout.findViewById(android.R.id.keyboardView) + ?: KeyboardView(context, theme) + .apply { + id = android.R.id.keyboardView + this@apply.keyboardActionListener = + object : KeyboardActionListener { + override fun onEvent(event: Event) { + keyboardActionListener?.onEvent(event) + dismissPopupKeyboard() + } + + override fun onKey( + keyEventCode: Int, + metaState: Int, + ) { + keyboardActionListener?.onKey(keyEventCode, metaState) + dismissPopupKeyboard() + } + + override fun onText(text: CharSequence) { + keyboardActionListener?.onText(text) + dismissPopupKeyboard() + } + + override fun onPress(keyEventCode: Int) { + Timber.d("onLongPress: onPress key=$keyEventCode") + keyboardActionListener?.onPress(keyEventCode) + } + + override fun onRelease(keyEventCode: Int) { + keyboardActionListener?.onRelease(keyEventCode) + } + } + keyboard = + if (popupKey.popupCharacters != null) { + Keyboard(theme, popupKey.popupCharacters, -1, paddingLeft + paddingRight) + } else { + Keyboard(theme) + } + mPopupParent = this + }.also { (miniKeyboardLayout as LinearLayout).run { add(it, lParams(matchParent, wrapContent)) } } - override fun onPress(keyEventCode: Int) { - Timber.d("onLongPress: onPress key=$keyEventCode") - keyboardActionListener?.onPress(keyEventCode) - } + miniKeyboardLayout.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST), + ) - override fun onRelease(keyEventCode: Int) { - keyboardActionListener?.onRelease(keyEventCode) - } - } - // mInputView.setSuggest(mSuggest); - val keyboard = - if (popupKey.popupCharacters != null) { - Keyboard(popupKey.popupCharacters, -1, paddingLeft + paddingRight) - } else { - Keyboard() - } - mMiniKeyboard.keyboard = keyboard - mMiniKeyboard.mPopupParent = this - mMiniKeyboardContainer.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST), - ) - mMiniKeyboardCache[popupKey] = mMiniKeyboardContainer - } else { - mMiniKeyboard = mMiniKeyboardContainer.findViewById(android.R.id.keyboardView) - } getLocationInWindow(mCoordinates) - var mPopupX = popupKey.x + paddingLeft - var mPopupY = popupKey.y + paddingTop - mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.measuredWidth - mPopupY -= mMiniKeyboardContainer.measuredHeight - val x = mPopupX + mMiniKeyboardContainer.paddingRight + mCoordinates[0] - val y = mPopupY + mMiniKeyboardContainer.paddingBottom + mCoordinates[1] - mMiniKeyboard.setPopupOffset(max(x, 0), y) - + val x = popupKey.run { x + width } + paddingLeft + miniKeyboardLayout.run { paddingRight - measuredWidth } + mCoordinates[0] + val y = popupKey.y + paddingTop + miniKeyboardLayout.run { paddingBottom - measuredHeight } + mCoordinates[1] + miniKeyboardView.setPopupOffset(max(x, 0), y) // todo 只处理了shift + miniKeyboardView.setShifted(false, mKeyboard?.isShifted ?: false) Timber.w("only set isShifted, no others modifier key") - mMiniKeyboard.setShifted(false, mKeyboard?.isShifted ?: false) - mPopupKeyboard.contentView = mMiniKeyboardContainer - mPopupKeyboard.width = mMiniKeyboardContainer.measuredWidth - mPopupKeyboard.height = mMiniKeyboardContainer.measuredHeight + + mPopupKeyboard.contentView = miniKeyboardLayout + mPopupKeyboard.width = miniKeyboardLayout.measuredWidth + mPopupKeyboard.height = miniKeyboardLayout.measuredHeight mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y) mMiniKeyboardOnScreen = true - // mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); invalidateAllKeys() return true } diff --git a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt index c492dd1542..63f3d81cef 100644 --- a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt @@ -54,7 +54,7 @@ class KeyboardWindow( } } - val mainKeyboardView by lazy { KeyboardView(context) } + val mainKeyboardView by lazy { KeyboardView(context, theme) } private lateinit var keyboardView: FrameLayout @@ -66,8 +66,8 @@ class KeyboardWindow( private val presetKeyboardIds = theme.presetKeyboards?.keys?.toTypedArray() ?: emptyArray() private val keyboardsCached: HashMap by lazy { hashMapOf( - "default" to Keyboard("default"), - "number" to Keyboard("number"), + "default" to Keyboard(theme, "default"), + "number" to Keyboard(theme, "number"), ) } private var currentKeyboardId = "" @@ -77,8 +77,8 @@ class KeyboardWindow( override fun onCreateView(): View { keyboardView = context.frameLayout() - keyboardView.apply { add(mainKeyboardView, lParams(matchParent, matchParent)) } attachKeyboard(evalKeyboard(".default")) + keyboardView.apply { add(mainKeyboardView, lParams(matchParent, matchParent)) } return keyboardView } @@ -161,7 +161,7 @@ class KeyboardWindow( if (keyboardsCached.containsKey(target)) { if (target == currentKeyboardId) return@execute } else { - keyboardsCached[target] = Keyboard(target) + keyboardsCached[target] = Keyboard(theme, target) } attachKeyboard(target) if (windowManager.isAttached(this)) { diff --git a/app/src/main/java/com/osfans/trime/ime/symbol/TabManager.kt b/app/src/main/java/com/osfans/trime/ime/symbol/TabManager.kt index 2bbbc21281..27bfbf98a6 100644 --- a/app/src/main/java/com/osfans/trime/ime/symbol/TabManager.kt +++ b/app/src/main/java/com/osfans/trime/ime/symbol/TabManager.kt @@ -5,7 +5,7 @@ package com.osfans.trime.ime.symbol import com.osfans.trime.data.schema.SchemaManager -import com.osfans.trime.data.theme.ThemeManager +import com.osfans.trime.data.theme.Theme import com.osfans.trime.util.config.ConfigItem import com.osfans.trime.util.config.ConfigList import com.osfans.trime.util.config.ConfigMap @@ -16,12 +16,12 @@ object TabManager { var currentTabIndex = -1 private set - val theme get() = ThemeManager.activeTheme val tabTags = arrayListOf() private val keyboards = mutableListOf>() - fun refresh() { - reset() + fun resetCache(theme: Theme) { + tabTags.clear() + keyboards.clear() val available = theme.liquid.getList("keyboards") ?: return for (item in available) { @@ -36,11 +36,6 @@ object TabManager { } } - private fun reset() { - tabTags.clear() - keyboards.clear() - } - private fun addListTab( name: String, type: SymbolBoardType, diff --git a/app/src/main/java/com/osfans/trime/ui/fragments/KeyboardFragment.kt b/app/src/main/java/com/osfans/trime/ui/fragments/KeyboardFragment.kt index 88fbb9d9ea..0725a864b1 100644 --- a/app/src/main/java/com/osfans/trime/ui/fragments/KeyboardFragment.kt +++ b/app/src/main/java/com/osfans/trime/ui/fragments/KeyboardFragment.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import com.osfans.trime.R import com.osfans.trime.data.prefs.AppPrefs +import com.osfans.trime.data.theme.ThemeManager import com.osfans.trime.ime.core.TrimeInputMethodService import com.osfans.trime.ui.components.PaddingPreferenceFragment import com.osfans.trime.ui.main.MainViewModel @@ -46,7 +47,7 @@ class KeyboardFragment : "keyboard__show_window", "keyboard__inline_preedit", "keyboard__soft_cursor", -> { - TrimeInputMethodService.getServiceOrNull()?.recreateInputView() + TrimeInputMethodService.getServiceOrNull()?.recreateInputView(ThemeManager.activeTheme) } } } diff --git a/app/src/main/res/layout/composition_root.xml b/app/src/main/res/layout/composition_root.xml deleted file mode 100644 index a6b88bbe8f..0000000000 --- a/app/src/main/res/layout/composition_root.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/keyboard_popup_keyboard.xml b/app/src/main/res/layout/keyboard_popup_keyboard.xml deleted file mode 100644 index 75fab17a3c..0000000000 --- a/app/src/main/res/layout/keyboard_popup_keyboard.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - From 6817f1eaf1ab26e6c998ea29c73025a1bed16b0e Mon Sep 17 00:00:00 2001 From: WhiredPlanck Date: Mon, 7 Oct 2024 11:37:50 +0800 Subject: [PATCH 2/4] refactors: enhance the process of theme switching refactors: add id for some view for the convenience of debugging --- .../java/com/osfans/trime/data/theme/Theme.kt | 3 -- .../osfans/trime/data/theme/ThemeManager.kt | 40 ++++++++++++++++--- .../com/osfans/trime/ime/core/InputView.kt | 3 +- .../trime/ime/core/TrimeInputMethodService.kt | 32 +++++++++------ .../trime/ime/keyboard/KeyboardWindow.kt | 3 +- .../trime/ime/window/BoardWindowManager.kt | 4 +- app/src/main/res/values/input_view_ids.xml | 2 + 7 files changed, 62 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/osfans/trime/data/theme/Theme.kt b/app/src/main/java/com/osfans/trime/data/theme/Theme.kt index 8ec71a9789..e3b1d76853 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/Theme.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/Theme.kt @@ -5,7 +5,6 @@ package com.osfans.trime.data.theme import com.osfans.trime.core.Rime -import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.data.theme.mapper.GeneralStyleMapper import com.osfans.trime.data.theme.model.GeneralStyle import com.osfans.trime.util.config.Config @@ -32,7 +31,6 @@ class Theme( private set companion object { - private val prefs = AppPrefs.defaultInstance().theme private const val VERSION_KEY = "config_version" private const val DEFAULT_THEME_NAME = "trime" @@ -70,7 +68,6 @@ class Theme( presetColorSchemes = config.getMap("preset_color_schemes") presetKeyboards = config.getMap("preset_keyboards") Timber.i("The theme is initialized") - prefs.selectedTheme = name } private fun mapToGeneralStyle(config: Config): GeneralStyle { diff --git a/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt b/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt index 4d972b780d..394a0b9b4d 100644 --- a/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt +++ b/app/src/main/java/com/osfans/trime/data/theme/ThemeManager.kt @@ -8,6 +8,7 @@ import androidx.annotation.Keep import com.osfans.trime.data.base.DataManager import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.ime.symbol.TabManager +import com.osfans.trime.util.WeakHashSet import java.io.File object ThemeManager { @@ -50,23 +51,50 @@ object ThemeManager { setNormalTheme(prefs.selectedTheme) } - lateinit var activeTheme: Theme - private set + private lateinit var _activeTheme: Theme + + var activeTheme: Theme + get() = _activeTheme + set(value) { + if (::_activeTheme.isInitialized && _activeTheme == value) return + _activeTheme = value + fireChange() + } + + private val onChangeListeners = WeakHashSet() + + fun addOnChangedListener(listener: OnThemeChangeListener) { + onChangeListeners.add(listener) + } + + fun removeOnChangedListener(listener: OnThemeChangeListener) { + onChangeListeners.remove(listener) + } + + private fun fireChange() { + onChangeListeners.forEach { it.onThemeChange(_activeTheme) } + } private val prefs = AppPrefs.defaultInstance().theme - fun init() = setNormalTheme(prefs.selectedTheme) + fun init() { + Theme(prefs.selectedTheme).let { + EventManager.resetCache() + FontManager.resetCache(it) + ColorManager.resetCache(it) + TabManager.resetCache(it) + _activeTheme = it + } + } fun setNormalTheme(name: String) { Theme(name).let { - if (::activeTheme.isInitialized) { - if (activeTheme == it) return - } EventManager.resetCache() FontManager.resetCache(it) ColorManager.resetCache(it) TabManager.resetCache(it) activeTheme = it } + prefs.selectedTheme = name } } diff --git a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt index 3376705fb2..60d729a25f 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt @@ -51,6 +51,7 @@ import splitties.views.dsl.constraintlayout.constraintLayout import splitties.views.dsl.constraintlayout.endOfParent import splitties.views.dsl.constraintlayout.endToStartOf import splitties.views.dsl.constraintlayout.lParams +import splitties.views.dsl.constraintlayout.matchConstraints import splitties.views.dsl.constraintlayout.startOfParent import splitties.views.dsl.constraintlayout.startToEndOf import splitties.views.dsl.constraintlayout.topOfParent @@ -236,7 +237,7 @@ class InputView( ) add( windowManager.view, - lParams(matchParent, wrapContent) { + lParams(matchConstraints, wrapContent) { below(quickBar.view) above(bottomPaddingSpace) }, diff --git a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt index 914dd99478..e4b5e1859a 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt @@ -29,6 +29,7 @@ import android.view.inputmethod.ExtractedTextRequest import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.Keep +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope @@ -76,8 +77,6 @@ import splitties.systemservices.inputMethodManager import splitties.views.gravityBottom import timber.log.Timber import java.util.Locale -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext /** [輸入法][InputMethodService]主程序 */ @@ -102,6 +101,12 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { var lastCommittedText: CharSequence = "" private set + @Keep + private val onThemeChangeListener = + ThemeManager.OnThemeChangeListener { + recreateInputView(it) + } + @Keep private val onColorChangeListener = ColorManager.OnColorChangeListener { @@ -111,11 +116,10 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { } private fun postJob( - ctx: CoroutineContext, scope: CoroutineScope, block: suspend () -> Unit, ): Job { - val job = scope.launch(ctx, CoroutineStart.LAZY) { block() } + val job = scope.launch(start = CoroutineStart.LAZY) { block() } jobs.trySend(job) return job } @@ -127,10 +131,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { * subsequent operations can start if the prior operation is not finished (suspended), * [postRimeJob] ensures that operations are executed sequentially. */ - fun postRimeJob( - ctx: CoroutineContext = EmptyCoroutineContext, - block: suspend RimeApi.() -> Unit, - ) = postJob(ctx, rime.lifecycleScope) { rime.runOnReady(block) } + fun postRimeJob(block: suspend RimeApi.() -> Unit) = postJob(rime.lifecycleScope) { rime.runOnReady(block) } override fun onWindowShown() { super.onWindowShown() @@ -222,6 +223,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { handleRimeResponse(it) } } + ThemeManager.addOnChangedListener(onThemeChangeListener) ColorManager.addOnChangedListener(onColorChangeListener) super.onCreate() instance = this @@ -324,6 +326,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { mIntentReceiver = null InputFeedbackManager.destroy() inputView = null + ThemeManager.removeOnChangedListener(onThemeChangeListener) ColorManager.removeOnChangedListener(onColorChangeListener) super.onDestroy() RimeDaemon.destroySession(javaClass.name) @@ -413,8 +416,10 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { override fun onCreateInputView(): View { Timber.d("onCreateInputView") - postRimeJob(Dispatchers.Main) { - recreateInputView(ThemeManager.activeTheme) + postRimeJob { + ContextCompat.getMainExecutor(this@TrimeInputMethodService).execute { + recreateInputView(ThemeManager.activeTheme) + } } initializationUi = InitializationUi(this) return initializationUi!!.root @@ -460,7 +465,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { restarting: Boolean, ) { Timber.d("onStartInputView: restarting=%s", restarting) - postRimeJob(Dispatchers.Main) { + postRimeJob { InputFeedbackManager.loadSoundEffects(this@TrimeInputMethodService) InputFeedbackManager.resetPlayProgress() isComposable = @@ -477,8 +482,9 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { if (prefs.other.showStatusBarIcon) { showStatusIcon(R.drawable.ic_trime_status) // 狀態欄圖標 } - setCandidatesViewShown(!rime.run { isEmpty() }) // 軟鍵盤出現時顯示候選欄 - inputView?.startInput(attribute, restarting) + ContextCompat.getMainExecutor(this@TrimeInputMethodService).execute { + inputView?.startInput(attribute, restarting) + } when (attribute.inputType and InputType.TYPE_MASK_VARIATION) { InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, InputType.TYPE_TEXT_VARIATION_PASSWORD, diff --git a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt index 63f3d81cef..888ba4d4c2 100644 --- a/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/com/osfans/trime/ime/keyboard/KeyboardWindow.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.widget.FrameLayout import androidx.core.content.ContextCompat +import com.osfans.trime.R import com.osfans.trime.core.Rime import com.osfans.trime.core.RimeNotification.OptionNotification import com.osfans.trime.daemon.RimeSession @@ -76,7 +77,7 @@ class KeyboardWindow( private val currentKeyboard: Keyboard? get() = keyboardsCached[currentKeyboardId] override fun onCreateView(): View { - keyboardView = context.frameLayout() + keyboardView = context.frameLayout(R.id.keyboard_view) attachKeyboard(evalKeyboard(".default")) keyboardView.apply { add(mainKeyboardView, lParams(matchParent, matchParent)) } return keyboardView diff --git a/app/src/main/java/com/osfans/trime/ime/window/BoardWindowManager.kt b/app/src/main/java/com/osfans/trime/ime/window/BoardWindowManager.kt index 383c4a0ea3..20b48d85bc 100644 --- a/app/src/main/java/com/osfans/trime/ime/window/BoardWindowManager.kt +++ b/app/src/main/java/com/osfans/trime/ime/window/BoardWindowManager.kt @@ -10,6 +10,7 @@ import android.widget.FrameLayout import androidx.transition.Transition import androidx.transition.TransitionManager import androidx.transition.TransitionSet +import com.osfans.trime.R import com.osfans.trime.ime.broadcast.InputBroadcaster import com.osfans.trime.ime.dependency.InputScope import me.tatarka.inject.annotations.Inject @@ -60,6 +61,7 @@ class BoardWindowManager( throw IllegalStateException("${window.key} is already occupied") } } + broadcaster.addReceiver(window) val view = if (createView) window.onCreateView() else null cachedResidentWindows[window.key] = window to view } @@ -111,7 +113,7 @@ class BoardWindowManager( broadcaster.onWindowAttached(window) } - val view: FrameLayout by lazy { context.frameLayout() } + val view: FrameLayout by lazy { context.frameLayout(R.id.input_window) } fun isAttached(window: BoardWindow) = currentWindow === window } diff --git a/app/src/main/res/values/input_view_ids.xml b/app/src/main/res/values/input_view_ids.xml index 48c8caebcf..e27faa8649 100644 --- a/app/src/main/res/values/input_view_ids.xml +++ b/app/src/main/res/values/input_view_ids.xml @@ -9,4 +9,6 @@ SPDX-License-Identifier: GPL-3.0-or-later + + From 57bf1259f9fbdfff91c025a9da6af4e06c708b06 Mon Sep 17 00:00:00 2001 From: WhiredPlanck Date: Mon, 7 Oct 2024 22:30:46 +0800 Subject: [PATCH 3/4] refactors: optimize timing background sync --- .../com/osfans/trime/data/prefs/AppPrefs.kt | 19 +- .../trime/ime/broadcast/IntentReceiver.kt | 2 +- .../trime/ime/core/TrimeInputMethodService.kt | 25 +-- .../ui/components/TimePickerPreference.kt | 97 ++++++++++ .../trime/ui/fragments/ProfileFragment.kt | 175 +++++------------- .../com/osfans/trime/util/ShortcutUtils.kt | 2 +- app/src/main/res/values-zh-rCN/strings.xml | 11 +- app/src/main/res/values-zh-rTW/strings.xml | 11 +- app/src/main/res/values/strings.xml | 11 +- app/src/main/res/xml/profile_preference.xml | 20 +- 10 files changed, 186 insertions(+), 187 deletions(-) create mode 100644 app/src/main/java/com/osfans/trime/ui/components/TimePickerPreference.kt diff --git a/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt b/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt index 8129df3f81..36369ac071 100644 --- a/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt +++ b/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt @@ -13,7 +13,6 @@ import com.osfans.trime.ime.enums.FullscreenMode import com.osfans.trime.ime.enums.InlinePreeditMode import com.osfans.trime.util.appContext import java.lang.ref.WeakReference -import java.util.Calendar /** * Helper class for an organized access to the shared preferences. @@ -198,19 +197,17 @@ class AppPrefs( ) : PreferenceDelegateOwner(shared) { companion object { const val USER_DATA_DIR = "profile_user_data_dir" - const val SYNC_BACKGROUND_ENABLED = "profile_sync_in_background" - const val TIMING_SYNC_ENABLED = "profile_timing_sync" - const val TIMING_SYNC_TRIGGER_TIME = "profile_timing_sync_trigger_time" - const val LAST_SYNC_STATUS = "profile_last_sync_status" - const val LAST_BACKGROUND_SYNC = "profile_last_background_sync" + const val TIMING_BACKGROUND_SYNC_ENABLED = "profile_timing_background_sync" + const val TIMING_BACKGROUND_SYNC_SET_TIME = "profile_timing_background_sync_set_time" + const val LAST_BACKGROUND_SYNC_STATUS = "profile_last_background_sync_status" + const val LAST_BACKGROUND_SYNC_TIME = "profile_last_background_sync_time" } var userDataDir by string(USER_DATA_DIR, DataManager.defaultDataDirectory.path) - var syncBackgroundEnabled by bool(SYNC_BACKGROUND_ENABLED, false) - var timingSyncEnabled by bool(TIMING_SYNC_ENABLED, false) - var timingSyncTriggerTime by long(TIMING_SYNC_TRIGGER_TIME, Calendar.getInstance().timeInMillis + 1200000L) - var lastSyncStatus by bool(LAST_SYNC_STATUS, false) - var lastBackgroundSync by string(LAST_BACKGROUND_SYNC, "") + var timingBackgroundSyncEnabled by bool(TIMING_BACKGROUND_SYNC_ENABLED, false) + var timingBackgroundSyncSetTime by long(TIMING_BACKGROUND_SYNC_SET_TIME, System.currentTimeMillis()) + var lastSyncStatus by bool(LAST_BACKGROUND_SYNC_STATUS, false) + var lastBackgroundSyncTime by long(LAST_BACKGROUND_SYNC_TIME, 0L) } class Clipboard( diff --git a/app/src/main/java/com/osfans/trime/ime/broadcast/IntentReceiver.kt b/app/src/main/java/com/osfans/trime/ime/broadcast/IntentReceiver.kt index d120661d1a..a4413bf979 100644 --- a/app/src/main/java/com/osfans/trime/ime/broadcast/IntentReceiver.kt +++ b/app/src/main/java/com/osfans/trime/ime/broadcast/IntentReceiver.kt @@ -65,7 +65,7 @@ class IntentReceiver : wakeLock.acquire(600000) // 10分钟超时 val cal = Calendar.getInstance() val triggerTime = cal.timeInMillis + TimeUnit.DAYS.toMillis(1) // 下次同步时间 - AppPrefs.defaultInstance().profile.timingSyncTriggerTime = + AppPrefs.defaultInstance().profile.timingBackgroundSyncSetTime = triggerTime // 更新定时同步偏好值 // 设置待发送的同步事件 val pendingIntent = diff --git a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt index e4b5e1859a..9b9407af0e 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt @@ -11,9 +11,6 @@ import android.content.Intent import android.content.res.Configuration import android.inputmethodservice.InputMethodService import android.os.Build -import android.os.Handler -import android.os.Looper -import android.os.Message import android.os.SystemClock import android.text.InputType import android.view.InputDevice @@ -160,11 +157,6 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { Timber.d("onWindowHidden") } isWindowShown = false - if (prefs.profile.syncBackgroundEnabled) { - val msg = Message() - msg.obj = this - syncBackgroundHandler.sendMessageDelayed(msg, 5000) // 输入面板隐藏5秒后,开始后台同步 - } } private fun updateRimeOption(): Boolean { @@ -184,8 +176,8 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { /** 防止重启系统 强行停止应用时alarm任务丢失 */ @SuppressLint("ScheduleExactAlarm") fun restartSystemStartTimingSync() { - if (prefs.profile.timingSyncEnabled) { - val triggerTime = prefs.profile.timingSyncTriggerTime + if (prefs.profile.timingBackgroundSyncEnabled) { + val triggerTime = prefs.profile.timingBackgroundSyncSetTime val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager /** 设置待发送的同步事件 */ @@ -1074,18 +1066,5 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { fun getService(): TrimeInputMethodService = instance ?: throw IllegalStateException("TrimeInputMethodService is not initialized") fun getServiceOrNull(): TrimeInputMethodService? = instance - - private val syncBackgroundHandler = - Handler( - Looper.getMainLooper(), - ) { msg: Message -> - // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开 - val service = msg.obj as TrimeInputMethodService - if (!service.isShowInputRequested) { - ShortcutUtils.syncInBackground() - service.shouldUpdateRimeOption = true - } - false - } } } diff --git a/app/src/main/java/com/osfans/trime/ui/components/TimePickerPreference.kt b/app/src/main/java/com/osfans/trime/ui/components/TimePickerPreference.kt new file mode 100644 index 0000000000..c93938d14d --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ui/components/TimePickerPreference.kt @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2015 - 2024 Rime community + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.osfans.trime.ui.components + +import android.app.TimePickerDialog +import android.content.Context +import android.util.AttributeSet +import androidx.core.content.edit +import androidx.preference.DialogPreference +import java.util.Calendar +import java.util.concurrent.TimeUnit + +class TimePickerPreference + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + ) : DialogPreference(context, attrs) { + var default: Long = System.currentTimeMillis() + + var value = System.currentTimeMillis() + private set + + override fun onSetInitialValue(defaultValue: Any?) { + super.onSetInitialValue(defaultValue) + preferenceDataStore?.apply { + value = getLong(key, default) + } ?: sharedPreferences?.apply { + value = getLong(key, default) + } + } + + override fun setDefaultValue(defaultValue: Any?) { + val time = defaultValue as? Long ?: return + value = time as? Long ?: System.currentTimeMillis() + } + + private fun persistValues(setTime: Long) { + if (!shouldPersist()) return + value = setTime + preferenceDataStore?.apply { + putLong(key, setTime) + } ?: sharedPreferences?.edit { + putLong(key, setTime) + } + } + + override fun onClick() { + showTimePickerDialog() + } + + private fun showTimePickerDialog() { + val cal = Calendar.getInstance() + val timeSetListener = // 监听时间选择器设置 + TimePickerDialog.OnTimeSetListener { _, hour, minute -> + cal.set(Calendar.HOUR_OF_DAY, hour) + cal.set(Calendar.MINUTE, minute) + val timeInMillis = cal.timeInMillis // 设置的时间 + val triggerAtMillis = + if (timeInMillis > System.currentTimeMillis() + PERIOD) { + timeInMillis + } else { + // 设置的时间小于当前时间 20 分钟时将同步推迟到明天 + timeInMillis + TimeUnit.DAYS.toMillis(1) + } + setValue(triggerAtMillis) + } + // 时间选择器设置 + TimePickerDialog( + context, + timeSetListener, + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + true, + ).apply { + setOnCancelListener { + // 当取消时间选择器时重置偏好 + setValue(0L) + } + show() + } + } + + private fun setValue(setTime: Long) { + if (callChangeListener(setTime)) { + persistValues(setTime) + notifyChanged() + } + } + + companion object { + private const val PERIOD = 1_200_000L // 20 分钟 + } + } diff --git a/app/src/main/java/com/osfans/trime/ui/fragments/ProfileFragment.kt b/app/src/main/java/com/osfans/trime/ui/fragments/ProfileFragment.kt index 611ef9fb13..d953a8a291 100644 --- a/app/src/main/java/com/osfans/trime/ui/fragments/ProfileFragment.kt +++ b/app/src/main/java/com/osfans/trime/ui/fragments/ProfileFragment.kt @@ -6,7 +6,6 @@ package com.osfans.trime.ui.fragments import android.app.AlarmManager import android.app.PendingIntent -import android.app.TimePickerDialog import android.content.Intent import android.content.SharedPreferences import android.os.Build.VERSION @@ -18,7 +17,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat +import androidx.preference.Preference.SummaryProvider import androidx.preference.get import com.osfans.trime.R import com.osfans.trime.core.Rime @@ -27,6 +26,7 @@ import com.osfans.trime.data.base.DataManager import com.osfans.trime.data.prefs.AppPrefs import com.osfans.trime.ui.components.FolderPickerPreference import com.osfans.trime.ui.components.PaddingPreferenceFragment +import com.osfans.trime.ui.components.TimePickerPreference import com.osfans.trime.ui.main.MainViewModel import com.osfans.trime.util.ResourceUtils import com.osfans.trime.util.appContext @@ -39,8 +39,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import splitties.systemservices.alarmManager -import java.util.Calendar -import java.util.concurrent.TimeUnit class ProfileFragment : PaddingPreferenceFragment(), @@ -81,118 +79,29 @@ class ProfileFragment : } true } - get("profile_sync_in_background")?.apply { - val lastBackgroundSync = prefs.profile.lastBackgroundSync - summaryOn = - if (lastBackgroundSync.isBlank()) { - context.getString(R.string.profile_never_sync_in_background) - } else { - context.getString( - R.string.profile_last_sync_in_background, - formatDateTime(lastBackgroundSync.toLong()), - context.getString( - if (prefs.profile.lastSyncStatus) { - R.string.success + get("profile_timing_background_sync_set_time")?.apply { + setDefaultValue(false to System.currentTimeMillis()) + summaryProvider = + SummaryProvider { + if (prefs.profile.timingBackgroundSyncEnabled) { + val (lastTime, lastStatus) = + if (prefs.profile.lastBackgroundSyncTime != 0L) { + formatDateTime(prefs.profile.lastBackgroundSyncTime) to + context.getString(if (prefs.profile.lastSyncStatus) R.string.success else R.string.failure) } else { - R.string.failure - }, - ), - ) - } - summaryOff = context.getString(R.string.profile_enable_syncing_in_background) - } - get("profile_timing_sync")?.apply { - // 定时同步偏好描述 - val timingSyncPreference: SwitchPreferenceCompat? = findPreference("profile_timing_sync") - timingSyncPreference?.summaryProvider = - Preference.SummaryProvider { - if (prefs.profile.timingSyncEnabled) { + "N/A" to "N/A" + } context.getString( - R.string.profile_timing_sync_trigger_time, - formatDateTime(prefs.profile.timingSyncTriggerTime), + R.string.profile_timing_background_sync_status, + lastTime, + lastStatus, + formatDateTime(prefs.profile.timingBackgroundSyncSetTime), ) } else { - context.getString(R.string.profile_enable_timing_sync) + context.getString(R.string.profile_timing_background_sync_hint) } } } - get("profile_timing_sync")?.setOnPreferenceClickListener { - // 监听定时同步偏好设置 - // 设置待发送的同步事件 - val pendingIntent = - PendingIntent.getBroadcast( - context, - 0, - Intent("com.osfans.trime.timing.sync"), - if (VERSION.SDK_INT >= VERSION_CODES.M) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - }, - ) - val cal = Calendar.getInstance() - if (get("profile_timing_sync")?.isChecked == true) { // 当定时同步偏好打开时 - val timeSetListener = // 监听时间选择器设置 - TimePickerDialog.OnTimeSetListener { _, hour, minute -> - cal.set(Calendar.HOUR_OF_DAY, hour) - cal.set(Calendar.MINUTE, minute) - val triggerTime = cal.timeInMillis // 设置的时间 - if (triggerTime > System.currentTimeMillis() + 1200000L) { // 设置的时间小于当前时间20分钟时将同步推迟到明天 - prefs.profile.timingSyncTriggerTime = triggerTime // 更新定时同步偏好值 - if (VERSION.SDK_INT < VERSION_CODES.S || alarmManager.canScheduleExactAlarms()) { - if (VERSION.SDK_INT >= VERSION_CODES.M) { // 根据 API Level 设置 alarm 任务 - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerTime, - pendingIntent, - ) - } else { - alarmManager.setExact( - AlarmManager.RTC_WAKEUP, - triggerTime, - pendingIntent, - ) - } - } - } else { - prefs.profile.timingSyncTriggerTime = - triggerTime + TimeUnit.DAYS.toMillis(1) - if (VERSION.SDK_INT < VERSION_CODES.S || alarmManager.canScheduleExactAlarms()) { - if (VERSION.SDK_INT >= VERSION_CODES.M) { - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerTime + TimeUnit.DAYS.toMillis(1), - pendingIntent, - ) - } else { - alarmManager.setExact( - AlarmManager.RTC_WAKEUP, - triggerTime + TimeUnit.DAYS.toMillis(1), - pendingIntent, - ) - } - } - } - } - // 时间选择器设置 - val tpDialog = - TimePickerDialog( - context, - timeSetListener, - cal.get(Calendar.HOUR_OF_DAY), - cal.get(Calendar.MINUTE), - true, - ) - tpDialog.setOnCancelListener { - // 当取消时间选择器时重置偏好 - get("profile_timing_sync")?.isChecked = false - } - tpDialog.show() - } else { - alarmManager.cancel(pendingIntent) // 取消alarm任务 - } - true - } get("profile_reset")?.setOnPreferenceClickListener { val base = "shared" val items = appContext.assets.list(base)!! @@ -228,23 +137,39 @@ class ProfileFragment : override fun onSharedPreferenceChanged( sharedPreferences: SharedPreferences?, key: String?, - ) { // 实时更新定时同步偏好描述 - val timingSyncPreference: SwitchPreferenceCompat? = findPreference("profile_timing_sync") - when (key) { - "profile_timing_sync_trigger_time", - -> { - timingSyncPreference?.summaryProvider = - Preference.SummaryProvider { - if (prefs.profile.timingSyncEnabled) { - context?.getString( - R.string.profile_timing_sync_trigger_time, - formatDateTime(prefs.profile.timingSyncTriggerTime), - ) - } else { - context?.getString(R.string.profile_enable_timing_sync) - } - } + ) { + // 监听定时同步偏好设置 + // 设置待发送的同步事件 + val pendingIntent = + PendingIntent.getBroadcast( + context, + 0, + Intent("com.osfans.trime.timing.sync"), + if (VERSION.SDK_INT >= VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + }, + ) + if (prefs.profile.timingBackgroundSyncEnabled) { + val timeAtMillis = prefs.profile.timingBackgroundSyncSetTime + if (VERSION.SDK_INT < VERSION_CODES.S || alarmManager.canScheduleExactAlarms()) { + if (VERSION.SDK_INT >= VERSION_CODES.M) { // 根据 API Level 设置 alarm 任务 + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + timeAtMillis, + pendingIntent, + ) + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + timeAtMillis, + pendingIntent, + ) + } } + } else { + alarmManager.cancel(pendingIntent) } } diff --git a/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt b/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt index 0320fef8bc..6002671b1c 100644 --- a/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt +++ b/app/src/main/java/com/osfans/trime/util/ShortcutUtils.kt @@ -143,7 +143,7 @@ object ShortcutUtils { fun syncInBackground() { val prefs = AppPrefs.defaultInstance() - prefs.profile.lastBackgroundSync = Date().time.toString() + prefs.profile.lastBackgroundSyncTime = Date().time CoroutineScope(Dispatchers.IO).launch { prefs.profile.lastSyncStatus = Rime.syncRimeUserData().also { RimeDaemon.restartRime() } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b44c623c1b..719f7f5c59 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -89,11 +89,10 @@ SPDX-License-Identifier: GPL-3.0-or-later 默认 共享文件夹 用户文件夹 - 后台同步 点击以开启后台同步 - 定时同步 - 已开启定时同步 下次同步时间: %1$s - 点击以开启定时同步 + 后台定时同步 + 上次同步时间:%1$s(%2$s)\n下次同步时间: %3$s + 后台定时同步已禁用 应用市场 用户社区 按键效果 @@ -198,12 +197,10 @@ SPDX-License-Identifier: GPL-3.0-or-later 关于 主题与配色 配置管理 - 从未在后台同步过 存储 维护 正在加载 恢复默认设置 - 上次后台同步时间:%1$s\n状态:%2$s 成功 失败 设定存储位置和修改同步设置等 @@ -267,4 +264,6 @@ SPDX-License-Identifier: GPL-3.0-or-later 同文风 设置向导 忘记该词 + 同步 + 设置后台同步时间 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 22d2c92403..d47591fb8d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -93,11 +93,10 @@ SPDX-License-Identifier: GPL-3.0-or-later 默認 共享資料夾 使用者資料夾 - 從未在後臺同步過 點擊以啟用後台同步 - 定時同步 - 已啟用定時同步 下次同步時間: %1$s - 點擊以啟用定時同步 + 後台定時同步 + 上次同步時間:%1$s(%2$s)\n下次同步時間: %3$s + 後台定時同步已禁用 應用市場 使用者社群 檢視 @@ -205,8 +204,6 @@ SPDX-License-Identifier: GPL-3.0-or-later 儲存 維護 正在載入 - 後臺同步 - 上次後臺同步時間:%1$s\n狀態:%2$s 成功 失敗 剪貼板內容 @@ -268,4 +265,6 @@ SPDX-License-Identifier: GPL-3.0-or-later 同文風 設定精靈 忘記該詞 + 同步 + 設定後台同步時間 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7118bf05d4..d0d257f387 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,13 +93,10 @@ SPDX-License-Identifier: GPL-3.0-or-later Free memory on quit Shared directory User directory - Sync in background - Last sync in background: %1$s\nStatus: %2$s - Never synced in the background Click to enable - Timing sync - Timing sync is enabled Next sync at time: %1$s - Click to enable + Timing background sync + Last: %1$s (%2$s)\nNext: %3$s + Timing background sync is disabled Marketplace User Community View @@ -268,4 +265,6 @@ SPDX-License-Identifier: GPL-3.0-or-later tongwenfeng Setup Wizard Forget this word + Synchronization + Set background sync time diff --git a/app/src/main/res/xml/profile_preference.xml b/app/src/main/res/xml/profile_preference.xml index 7b4ad68caa..ec219b563f 100644 --- a/app/src/main/res/xml/profile_preference.xml +++ b/app/src/main/res/xml/profile_preference.xml @@ -20,23 +20,27 @@ SPDX-License-Identifier: GPL-3.0-or-later app:useSimpleSummaryProvider="true" /> - + - + android:title="@string/profile_timing_background_sync" /> - + android:title="@string/profile_timing_background_sync_set_time" + app:dependency="profile_timing_background_sync" /> + + Date: Tue, 8 Oct 2024 20:14:54 +0800 Subject: [PATCH 4/4] fix: metrics of strings in RimeProto were not completely converted --- .../java/com/osfans/trime/core/RimeProto.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/osfans/trime/core/RimeProto.kt b/app/src/main/java/com/osfans/trime/core/RimeProto.kt index f789831aae..5f6004aba4 100644 --- a/app/src/main/java/com/osfans/trime/core/RimeProto.kt +++ b/app/src/main/java/com/osfans/trime/core/RimeProto.kt @@ -19,16 +19,29 @@ class RimeProto { val composition: Composition, val menu: Menu, val input: String, - val caretPos: Int, + private val _caretPos: Int, ) { + /** + * Same with [Composition.cursorPos], just directly assign to it. + */ + val caretPos = composition.cursorPos + data class Composition( - val length: Int = 0, - val cursorPos: Int = 0, + private val _length: Int = 0, + private val _cursorPos: Int = 0, private val _selStart: Int = 0, private val _selEnd: Int = 0, val preedit: String? = null, val commitTextPreview: String? = null, ) { + /** + * Actually we can directly use [String.length] on [preedit], but + * we add it here for the sake of completeness as it is semantically correct + */ + val length: Int = preedit.run { if (isNullOrEmpty()) 0 else String(toByteArray(), 0, _length).length } + + val cursorPos: Int = preedit.run { if (isNullOrEmpty()) 0 else String(toByteArray(), 0, _cursorPos).length } + val selStart: Int = preedit.run { if (isNullOrEmpty()) 0 else String(toByteArray(), 0, _selStart).length } val selEnd: Int = preedit.run { if (isNullOrEmpty()) 0 else String(toByteArray(), 0, _selEnd).length }