diff --git a/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt b/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt index ee8d677b0b..a4ecab3946 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/EditorInstance.kt @@ -32,7 +32,7 @@ class EditorInstance(private val ims: InputMethodService) { } } val textInputManager: TextInputManager - get() = (ims as Trime).textInputManager + get() = (ims as Trime).textInputManager ?: error("TextInputManager is null") var lastCommittedText: CharSequence = "" diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.java b/app/src/main/java/com/osfans/trime/ime/core/Trime.java deleted file mode 100644 index 33132ecbca..0000000000 --- a/app/src/main/java/com/osfans/trime/ime/core/Trime.java +++ /dev/null @@ -1,1260 +0,0 @@ -/* - * Copyright (C) 2015-present, osfans - * waxaca@163.com https://github.com/osfans - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.osfans.trime.ime.core; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.inputmethodservice.InputMethodService; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.text.InputType; -import android.text.TextUtils; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.inputmethod.CursorAnchorInfo; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; -import android.view.inputmethod.InputConnection; -import android.widget.FrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.osfans.trime.BuildConfig; -import com.osfans.trime.R; -import com.osfans.trime.core.Rime; -import com.osfans.trime.data.AppPrefs; -import com.osfans.trime.data.db.DraftHelper; -import com.osfans.trime.data.sound.SoundThemeManager; -import com.osfans.trime.data.theme.Theme; -import com.osfans.trime.databinding.CompositionRootBinding; -import com.osfans.trime.ime.broadcast.IntentReceiver; -import com.osfans.trime.ime.enums.Keycode; -import com.osfans.trime.ime.enums.SymbolKeyboardType; -import com.osfans.trime.ime.keyboard.Event; -import com.osfans.trime.ime.keyboard.InitializationUi; -import com.osfans.trime.ime.keyboard.InputFeedbackManager; -import com.osfans.trime.ime.keyboard.Key; -import com.osfans.trime.ime.keyboard.Keyboard; -import com.osfans.trime.ime.keyboard.KeyboardSwitcher; -import com.osfans.trime.ime.keyboard.KeyboardView; -import com.osfans.trime.ime.keyboard.KeyboardWindow; -import com.osfans.trime.ime.lifecycle.LifecycleInputMethodService; -import com.osfans.trime.ime.symbol.LiquidKeyboard; -import com.osfans.trime.ime.symbol.TabManager; -import com.osfans.trime.ime.symbol.TabView; -import com.osfans.trime.ime.text.Candidate; -import com.osfans.trime.ime.text.Composition; -import com.osfans.trime.ime.text.CompositionPopupWindow; -import com.osfans.trime.ime.text.ScrollView; -import com.osfans.trime.ime.text.TextInputManager; -import com.osfans.trime.ime.util.UiUtil; -import com.osfans.trime.util.DimensionsKt; -import com.osfans.trime.util.ShortcutUtils; -import com.osfans.trime.util.StringUtils; -import com.osfans.trime.util.ViewUtils; -import com.osfans.trime.util.WeakHashSet; -import java.util.Objects; -import splitties.bitflags.BitFlagsKt; -import splitties.systemservices.SystemServicesKt; -import timber.log.Timber; - -/** {@link InputMethodService 輸入法}主程序 */ -public class Trime extends LifecycleInputMethodService { - private static Trime self = null; - private LiquidKeyboard liquidKeyboard; - private boolean normalTextEditor; - - @NonNull - private AppPrefs getPrefs() { - return AppPrefs.defaultInstance(); - } - - private boolean darkMode; // 当前键盘主题是否处于暗黑模式 - private KeyboardView mainKeyboardView; // 主軟鍵盤 - - private Candidate mCandidate; // 候選 - private Composition mComposition; // 編碼 - private CompositionRootBinding compositionRootBinding = null; - private ScrollView mCandidateRoot, mTabRoot; - private TabView tabView; - - @Nullable public InputView inputView = null; - public WeakHashSet eventListeners = new WeakHashSet<>(); - public InputFeedbackManager inputFeedbackManager = null; // 效果管理器 - private IntentReceiver mIntentReceiver = null; - - public EditorInfo editorInfo = null; - - private boolean isWindowShown = false; // 键盘窗口是否已显示 - - private boolean isAutoCaps; // 句首自動大寫 - - public EditorInstance activeEditorInstance; - public TextInputManager textInputManager; // 文字输入管理器 - - private int minPopupSize; // 上悬浮窗的候选词的最小词长 - private int minPopupCheckSize; // 第一屏候选词数量少于设定值,则候选词上悬浮窗。(也就是说,第一屏存在长词)此选项大于1时,min_length等参数失效 - private CompositionPopupWindow mCompositionPopupWindow; - - public static Trime getService() { - return self; - } - - @Nullable - public static Trime getServiceOrNull() { - return self; - } - - private static final Handler syncBackgroundHandler = - new Handler( - Looper.getMainLooper(), - msg -> { - if (!((Trime) msg.obj).isShowInputRequested()) { // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开 - ShortcutUtils.INSTANCE.syncInBackground(); - ((Trime) msg.obj).loadConfig(); - } - return false; - }); - - public Trime() { - try { - self = this; - } catch (Exception e) { - Timber.e(e); - } - } - - @Override - public void onWindowShown() { - super.onWindowShown(); - if (isWindowShown) { - Timber.i("Ignoring (is already shown)"); - return; - } else { - Timber.i("onWindowShown..."); - } - - if (RimeWrapper.isReady() && activeEditorInstance != null) { - isWindowShown = true; - updateComposing(); - - for (EventListener listener : eventListeners) { - listener.onWindowShown(); - } - } - } - - @Override - public void onWindowHidden() { - super.onWindowHidden(); - if (!isWindowShown) { - Timber.d("Ignoring (window is already hidden)"); - return; - } else { - Timber.d("onWindowHidden"); - } - isWindowShown = false; - - if (getPrefs().getProfile().getSyncBackgroundEnabled()) { - final Message msg = new Message(); - msg.obj = this; - syncBackgroundHandler.sendMessageDelayed(msg, 5000); // 输入面板隐藏5秒后,开始后台同步 - } - - for (EventListener listener : eventListeners) { - listener.onWindowHidden(); - } - } - - public void updatePopupWindow(final int offsetX, final int offsetY) { - mCompositionPopupWindow.updatePopupWindow(offsetX, offsetY); - } - - public void loadConfig() { - final Theme theme = Theme.get(); - minPopupSize = theme.style.getInt("layout/min_length"); - minPopupCheckSize = theme.style.getInt("layout/min_check"); - textInputManager.setShouldResetAsciiMode(theme.style.getBoolean("reset_ascii_mode")); - isAutoCaps = theme.style.getBoolean("auto_caps"); - textInputManager.setShouldUpdateRimeOption(true); - - mCompositionPopupWindow.loadConfig(theme, getPrefs()); - } - - @SuppressWarnings("UnusedReturnValue") - private boolean updateRimeOption() { - try { - if (textInputManager.getShouldUpdateRimeOption()) { - Rime.setOption("soft_cursor", getPrefs().getKeyboard().getSoftCursorEnabled()); // 軟光標 - Rime.setOption("_horizontal", Theme.get().style.getBoolean("horizontal")); // 水平模式 - textInputManager.setShouldUpdateRimeOption(false); - } - } catch (Exception e) { - e.printStackTrace(); - return false; - } - return true; - } - - public void restartSystemStartTimingSync() { // 防止重启系统 强行停止应用时alarm任务丢失 - if (getPrefs().getProfile().getTimingSyncEnabled()) { - long triggerTime = getPrefs().getProfile().getTimingSyncTriggerTime(); - AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); - PendingIntent pendingIntent = - PendingIntent.getBroadcast( // 设置待发送的同步事件 - this, - 0, - new Intent("com.osfans.trime.timing.sync"), - VERSION.SDK_INT >= VERSION_CODES.M - ? (PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE) - : PendingIntent.FLAG_UPDATE_CURRENT); - if (VERSION.SDK_INT >= VERSION_CODES.M) { // 根据SDK设置alarm任务 - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); - } else { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); - } - } - } - - @Override - public void onCreate() { - super.onCreate(); - // MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops - try { - // Additional try..catch wrapper as the event listeners chain or the super.onCreate() method - // could crash - // and lead to a crash loop - Timber.d("onCreate"); - final InputMethodService context = this; - RimeWrapper.INSTANCE.startup( - () -> { - Timber.d("Running Trime.onCreate"); - textInputManager = - TextInputManager.Companion.getInstance(UiUtil.INSTANCE.isDarkMode(context)); - activeEditorInstance = new EditorInstance(context); - inputFeedbackManager = new InputFeedbackManager(context); - liquidKeyboard = new LiquidKeyboard(context); - mCompositionPopupWindow = new CompositionPopupWindow(); - restartSystemStartTimingSync(); - - try { - for (EventListener listener : eventListeners) { - listener.onCreate(); - } - } catch (Exception e) { - Timber.e(e); - } - Timber.d("Trime.onCreate completed"); - }); - } catch (Exception e) { - Timber.e(e); - } - } - - /** - * 变更配置的暗黑模式开关 - * - * @param darkMode 设置为暗黑模式 - * @return 模式实际上是否有发生变更 - */ - public boolean setDarkMode(boolean darkMode) { - if (darkMode != this.darkMode) { - Timber.d("Dark mode changed: %s", darkMode); - this.darkMode = darkMode; - return true; - } - Timber.d("Dark mode not changed: %s", darkMode); - return false; - } - - private SymbolKeyboardType symbolKeyboardType = SymbolKeyboardType.NO_KEY; - - public void inputSymbol(final String text) { - textInputManager.onPress(KeyEvent.KEYCODE_UNKNOWN); - if (Rime.isAsciiMode()) Rime.setOption("ascii_mode", false); - boolean asciiPunch = Rime.isAsciiPunch(); - if (asciiPunch) Rime.setOption("ascii_punct", false); - textInputManager.onText("{Escape}" + text); - if (asciiPunch) Rime.setOption("ascii_punct", true); - Trime.getService().selectLiquidKeyboard(-1); - } - - public void selectLiquidKeyboard(final int tabIndex) { - if (inputView == null) return; - if (tabIndex >= 0) { - inputView.switchUiByState(KeyboardWindow.State.Symbol); - - symbolKeyboardType = liquidKeyboard.select(tabIndex); - tabView.updateTabWidth(); - - mTabRoot.setBackground(mCandidateRoot.getBackground()); - mTabRoot.move(tabView.getHightlightLeft(), tabView.getHightlightRight()); - showLiquidKeyboardToolbar(); - } else { - symbolKeyboardType = SymbolKeyboardType.NO_KEY; - // 设置液体键盘处于隐藏状态 - TabManager.get().setTabExited(); - inputView.switchUiByState(KeyboardWindow.State.Main); - updateComposing(); - } - } - - // 按键需要通过tab name来打开liquidKeyboard的指定tab - public void selectLiquidKeyboard(@NonNull String name) { - if (name.matches("-?\\d+")) selectLiquidKeyboard(Integer.parseInt(name)); - else if (name.matches("[A-Z]+")) selectLiquidKeyboard(SymbolKeyboardType.valueOf(name)); - else selectLiquidKeyboard(TabManager.getTagIndex(name)); - } - - public void selectLiquidKeyboard(SymbolKeyboardType type) { - selectLiquidKeyboard(TabManager.getTagIndex(type)); - } - - public void pasteByChar() { - commitTextByChar(Objects.requireNonNull(ShortcutUtils.pasteFromClipboard(this)).toString()); - } - - public void invalidate() { - Rime.getInstance(false); - Theme.get().destroy(); - reset(); - textInputManager.setShouldUpdateRimeOption(true); - } - - private void showCompositionView(boolean isCandidate) { - if (TextUtils.isEmpty(Rime.getCompositionText()) && isCandidate) { - mCompositionPopupWindow.hideCompositionView(); - return; - } - compositionRootBinding.compositionRoot.measure( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - mCompositionPopupWindow.updateCompositionView( - compositionRootBinding.compositionRoot.getMeasuredWidth(), - compositionRootBinding.compositionRoot.getMeasuredHeight()); - } - - public void loadBackground() { - final Theme theme = Theme.get(); - final int orientation = getResources().getConfiguration().orientation; - - final Drawable textBackground = - theme.colors.getDrawable( - "text_back_color", - "layout/border", - "border_color", - "layout/round_corner", - "layout/alpha"); - mCompositionPopupWindow.setThemeStyle( - (int) DimensionsKt.dp2px(theme.style.getFloat("layout/elevation")), textBackground); - - if (mCandidateRoot != null) { - final Drawable candidateBackground = - theme.colors.getDrawable( - "candidate_background", - "candidate_border", - "candidate_border_color", - "candidate_border_round", - null); - if (candidateBackground != null) mCandidateRoot.setBackground(candidateBackground); - } - - if (inputView == null) return; - - // 单手键盘模式 - int oneHandMode = 0; - int[] padding = - theme.getKeyboardPadding(oneHandMode, orientation == Configuration.ORIENTATION_LANDSCAPE); - Timber.i( - "update KeyboardPadding: Trime.loadBackground, padding= %s %s %s, orientation=%s", - padding[0], padding[1], padding[2], orientation); - mainKeyboardView.setPadding(padding[0], 0, padding[1], padding[2]); - - final Drawable inputRootBackground = theme.colors.getDrawable("root_background"); - if (inputRootBackground != null) { - inputView.getKeyboardView().setBackground(inputRootBackground); - } else { - // 避免因为键盘整体透明而造成的异常 - inputView.getKeyboardView().setBackgroundColor(Color.BLACK); - } - - tabView.reset(); - } - - public void resetKeyboard() { - if (mainKeyboardView != null) { - mainKeyboardView.setShowHint(!Rime.getOption("_hide_key_hint")); - mainKeyboardView.setShowSymbol(!Rime.getOption("_hide_key_symbol")); - mainKeyboardView.reset(); // 實體鍵盤無軟鍵盤 - } - } - - public void resetCandidate() { - if (mCandidateRoot != null) { - loadBackground(); - setShowComment(!Rime.getOption("_hide_comment")); - mCandidateRoot.setVisibility(!Rime.getOption("_hide_candidate") ? View.VISIBLE : View.GONE); - mCandidate.reset(); - mCompositionPopupWindow.setPopupWindowEnabled( - getPrefs().getKeyboard().getPopupWindowEnabled() - && Theme.get().style.getObject("window") != null); - mComposition.setVisibility( - mCompositionPopupWindow.isPopupWindowEnabled() ? View.VISIBLE : View.GONE); - mComposition.reset(); - } - } - - /** 重置鍵盤、候選條、狀態欄等 !!注意,如果其中調用Rime.setOption,切換方案會卡住 */ - private void reset() { - if (inputView == null) return; - inputView.switchUiByState(KeyboardWindow.State.Main); - loadConfig(); - updateDarkMode(); - final Theme theme = Theme.get(darkMode); - theme.initCurrentColors(darkMode); - SoundThemeManager.switchSound(theme.colors.getString("sound")); - KeyboardSwitcher.newOrReset(); - resetCandidate(); - mCompositionPopupWindow.hideCompositionView(); - resetKeyboard(); - } - - /** Must be called on the UI thread */ - public void initKeyboard() { - if (textInputManager != null) { - reset(); - // setNavBarColor(); - textInputManager.setShouldUpdateRimeOption(true); // 不能在Rime.onMessage中調用set_option,會卡死 - bindKeyboardToInputView(); - // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground(); - updateComposing(); // 切換主題時刷新候選 - } - } - - public void initKeyboardDarkMode(boolean darkMode) { - final Theme theme = Theme.get(); - if (theme.getHasDarkLight()) { - loadConfig(); - theme.initCurrentColors(darkMode); - SoundThemeManager.switchSound(theme.colors.getString("sound")); - KeyboardSwitcher.newOrReset(); - resetCandidate(); - mCompositionPopupWindow.hideCompositionView(); - resetKeyboard(); - - // setNavBarColor(); - textInputManager.setShouldUpdateRimeOption(true); // 不能在Rime.onMessage中調用set_option,會卡死 - bindKeyboardToInputView(); - // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground(); - updateComposing(); // 切換主題時刷新候選 - } - } - - @Override - public void onDestroy() { - if (mIntentReceiver != null) mIntentReceiver.unregisterReceiver(this); - mIntentReceiver = null; - if (inputFeedbackManager != null) inputFeedbackManager.destroy(); - inputFeedbackManager = null; - inputView = null; - - for (EventListener listener : eventListeners) { - listener.onDestroy(); - } - eventListeners.clear(); - if (mCompositionPopupWindow != null) { - mCompositionPopupWindow.destroy(); - } - super.onDestroy(); - - self = null; - } - - private void handleReturnKey() { - if (editorInfo == null) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER); - return; - } - if ((editorInfo.inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_NULL) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER); - return; - } - if (BitFlagsKt.hasFlag(editorInfo.imeOptions, EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { - final InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.commitText("\n", 1); - return; - } - if (!TextUtils.isEmpty(editorInfo.actionLabel) - && editorInfo.actionId != EditorInfo.IME_ACTION_UNSPECIFIED) { - final InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.performEditorAction(editorInfo.actionId); - return; - } - final int action = editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; - final InputConnection ic = getCurrentInputConnection(); - switch (action) { - case EditorInfo.IME_ACTION_UNSPECIFIED: - case EditorInfo.IME_ACTION_NONE: - if (ic != null) ic.commitText("\n", 1); - break; - default: - if (ic != null) ic.performEditorAction(action); - break; - } - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - final Configuration config = getResources().getConfiguration(); - if (config != null) { - if (config.orientation != newConfig.orientation) { - // Clear composing text and candidates for orientation change. - performEscape(); - config.orientation = newConfig.orientation; - } - } - super.onConfigurationChanged(newConfig); - } - - @Override - public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { - mCompositionPopupWindow.updateCursorAnchorInfo(cursorAnchorInfo); - if (mCandidateRoot != null) { - showCompositionView(true); - } - } - - @Override - public void onUpdateSelection( - int oldSelStart, - int oldSelEnd, - int newSelStart, - int newSelEnd, - int candidatesStart, - int candidatesEnd) { - super.onUpdateSelection( - oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); - if ((candidatesEnd != -1) && ((newSelStart != candidatesEnd) || (newSelEnd != candidatesEnd))) { - // 移動光標時,更新候選區 - if ((newSelEnd < candidatesEnd) && (newSelEnd >= candidatesStart)) { - final int n = newSelEnd - candidatesStart; - Rime.setCaretPos(n); - updateComposing(); - } - } - if ((candidatesStart == -1 && candidatesEnd == -1) && (newSelStart == 0 && newSelEnd == 0)) { - // 上屏後,清除候選區 - performEscape(); - } - // Update the caps-lock status for the current cursor position. - dispatchCapsStateToInputView(); - } - - @Override - public void onComputeInsets(@NonNull InputMethodService.Insets outInsets) { - int[] location = new int[] {0, 0}; - if (inputView != null) { - inputView.getKeyboardView().getLocationInWindow(location); - } - int y = location[1]; - outInsets.contentTopInsets = y; - outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT; - outInsets.touchableRegion.setEmpty(); - outInsets.visibleTopInsets = y; - } - - @Override - public View onCreateInputView() { - Timber.d("onCreateInputView()"); - RimeWrapper.runAfterStarted( - () -> { - inputView = new InputView(this); - - mainKeyboardView = inputView.getKeyboardWindow().getOldMainInputView().mainKeyboardView; - // 初始化候选栏 - mCandidateRoot = inputView.getQuickBar().getOldCandidateBar().getRoot(); - mCandidate = inputView.getQuickBar().getOldCandidateBar().candidates; - - // 候选词悬浮窗的容器 - compositionRootBinding = CompositionRootBinding.inflate(LayoutInflater.from(this)); - mComposition = compositionRootBinding.compositions; - mCompositionPopupWindow.init(compositionRootBinding.compositionRoot, mCandidateRoot); - mTabRoot = inputView.getQuickBar().getOldTabBar().getRoot(); - - updateDarkMode(); - Theme.get(darkMode).initCurrentColors(darkMode); - - liquidKeyboard.setKeyboardView( - inputView.getKeyboardWindow().getOldSymbolInputView().liquidKeyboardView); - tabView = inputView.getQuickBar().getOldTabBar().tabs; - - for (EventListener listener : eventListeners) { - listener.onInitializeInputUi(inputView); - } - loadBackground(); - - KeyboardSwitcher.newOrReset(); - bindKeyboardToInputView(); - - setInputView(inputView); - Timber.d("onCreateInputView - completely ended"); - }); - Timber.d("onCreateInputView() finish"); - - return new InitializationUi(this).getRoot(); - } - - @Override - public void setInputView(View view) { - final FrameLayout inputArea = - Objects.requireNonNull(getWindow().getWindow()) - .getDecorView() - .findViewById(android.R.id.inputArea); - ViewGroup.LayoutParams lP1 = inputArea.getLayoutParams(); - lP1.height = ViewGroup.LayoutParams.MATCH_PARENT; - inputArea.setLayoutParams(lP1); - super.setInputView(view); - ViewGroup.LayoutParams lP2 = view.getLayoutParams(); - lP2.height = ViewGroup.LayoutParams.MATCH_PARENT; - view.setLayoutParams(lP2); - } - - @Override - public void onConfigureWindow( - @NonNull Window win, boolean isFullscreen, boolean isCandidatesOnly) { - win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - } - - public void setShowComment(boolean show_comment) { - if (mCandidateRoot != null) mCandidate.setShowComment(show_comment); - mComposition.setShowComment(show_comment); - } - - @Override - public void onStartInput(EditorInfo attribute, boolean restarting) { - editorInfo = attribute; - Timber.d("onStartInput: restarting=%s", restarting); - } - - private boolean updateDarkMode() { - boolean isDarkMode = UiUtil.INSTANCE.isDarkMode(this); - - return setDarkMode(isDarkMode); - } - - @Override - public void onStartInputView(EditorInfo attribute, boolean restarting) { - Timber.d("onStartInputView: restarting=%s", restarting); - editorInfo = attribute; - - RimeWrapper.runAfterStarted( - () -> { - if (updateDarkMode()) { - initKeyboardDarkMode(darkMode); - } - - inputFeedbackManager.resumeSoundPool(); - inputFeedbackManager.resetPlayProgress(); - for (EventListener listener : eventListeners) { - listener.onStartInputView(activeEditorInstance, restarting); - } - if (getPrefs().getOther().getShowStatusBarIcon()) { - showStatusIcon(R.drawable.ic_trime_status); // 狀態欄圖標 - } - bindKeyboardToInputView(); - // if (!restarting) setNavBarColor(); - setCandidatesViewShown(!Rime.isEmpty()); // 軟鍵盤出現時顯示候選欄 - - if ((attribute.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) - == EditorInfo.IME_FLAG_NO_ENTER_ACTION) { - mainKeyboardView.resetEnterLabel(); - } else { - mainKeyboardView.setEnterLabel( - attribute.imeOptions & EditorInfo.IME_MASK_ACTION, attribute.actionLabel); - } - - switch (attribute.inputType & InputType.TYPE_MASK_VARIATION) { - case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: - case InputType.TYPE_TEXT_VARIATION_PASSWORD: - case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: - case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: - case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: - Timber.i( - "EditorInfo: private;" - + " packageName=" - + attribute.packageName - + "; fieldName=" - + attribute.fieldName - + "; actionLabel=" - + attribute.actionLabel - + "; inputType=" - + attribute.inputType - + "; VARIATION=" - + (attribute.inputType & InputType.TYPE_MASK_VARIATION) - + "; CLASS=" - + (attribute.inputType & InputType.TYPE_MASK_CLASS) - + "; ACTION=" - + (attribute.imeOptions & EditorInfo.IME_MASK_ACTION)); - normalTextEditor = false; - break; - - default: - Timber.i( - "EditorInfo: normal;" - + " packageName=" - + attribute.packageName - + "; fieldName=" - + attribute.fieldName - + "; actionLabel=" - + attribute.actionLabel - + "; inputType=" - + attribute.inputType - + "; VARIATION=" - + (attribute.inputType & InputType.TYPE_MASK_VARIATION) - + "; CLASS=" - + (attribute.inputType & InputType.TYPE_MASK_CLASS) - + "; ACTION=" - + (attribute.imeOptions & EditorInfo.IME_MASK_ACTION)); - - if ((attribute.imeOptions & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) - == EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) { - // 应用程求以隐身模式打开键盘应用程序 - normalTextEditor = false; - Timber.d("EditorInfo: normal -> private, IME_FLAG_NO_PERSONALIZED_LEARNING"); - } else if (attribute.packageName.equals(BuildConfig.APPLICATION_ID) - || getPrefs() - .getClipboard() - .getDraftExcludeApp() - .contains(attribute.packageName)) { - normalTextEditor = false; - Timber.d("EditorInfo: normal -> exclude, packageName=%s", attribute.packageName); - } else { - normalTextEditor = true; - DraftHelper.INSTANCE.onInputEventChanged(); - } - } - }); - RimeWrapper.INSTANCE.runCheck(); - } - - @Override - public void onFinishInputView(boolean finishingInput) { - super.onFinishInputView(finishingInput); - if (RimeWrapper.isReady()) { - if (normalTextEditor) { - DraftHelper.INSTANCE.onInputEventChanged(); - } - try { - // Dismiss any pop-ups when the input-view is being finished and hidden. - mainKeyboardView.closing(); - performEscape(); - if (inputFeedbackManager != null) { - inputFeedbackManager.releaseSoundPool(); - } - mCompositionPopupWindow.hideCompositionView(); - } catch (Exception e) { - Timber.e(e, "Failed to show the PopupWindow."); - } - } - if (inputView != null) { - inputView.finishInput(); - } - Timber.d("OnFinishInputView"); - } - - @Override - public void onFinishInput() { - editorInfo = null; - super.onFinishInput(); - } - - public void bindKeyboardToInputView() { - if (mainKeyboardView != null) { - // Bind the selected keyboard to the input view. - Keyboard sk = KeyboardSwitcher.getCurrentKeyboard(); - mainKeyboardView.setKeyboard(sk); - dispatchCapsStateToInputView(); - } - } - - /** - * Dispatches cursor caps info to input view in order to implement auto caps lock at the start of - * a sentence. - */ - private void dispatchCapsStateToInputView() { - if ((isAutoCaps && Rime.isAsciiMode()) - && (mainKeyboardView != null && !mainKeyboardView.isCapsOn())) { - mainKeyboardView.setShifted(false, activeEditorInstance.getCursorCapsMode() != 0); - } - } - - private boolean isComposing() { - return Rime.isComposing(); - } - - /** - * Commit the current composing text together with the new text - * - * @param text the new text to be committed - */ - public void commitText(String text) { - getCurrentInputConnection().finishComposingText(); - activeEditorInstance.commitText(text, true); - } - - public void commitTextByChar(String text) { - for (int i = 0; i < text.length(); i++) { - if (!activeEditorInstance.commitText(text.substring(i, i + 1), false)) break; - } - } - - /** - * 如果爲{@link KeyEvent#KEYCODE_BACK Back鍵},則隱藏鍵盤 - * - * @param keyCode {@link KeyEvent#getKeyCode() 鍵碼} - * @return 是否處理了Back鍵事件 - */ - private boolean handleBack(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { - requestHideSelf(0); - return true; - } - return false; - } - - public boolean onRimeKey(int[] event) { - updateRimeOption(); - // todo 改为异步处理按键事件、刷新UI - final boolean ret = Rime.processKey(event[0], event[1]); - activeEditorInstance.commitRimeText(); - return ret; - } - - private boolean composeEvent(@NonNull KeyEvent event) { - if (textInputManager == null) { - return false; - } - final int keyCode = event.getKeyCode(); - if (keyCode == KeyEvent.KEYCODE_MENU) return false; // 不處理 Menu 鍵 - if (!Keycode.Companion.isStdKey(keyCode)) return false; // 只處理安卓標準按鍵 - if (event.getRepeatCount() == 0 && Key.isTrimeModifierKey(keyCode)) { - boolean ret = - onRimeKey( - Event.getRimeEvent( - keyCode, - event.getAction() == KeyEvent.ACTION_DOWN - ? event.getModifiers() - : Rime.META_RELEASE_ON)); - if (isComposing()) setCandidatesViewShown(textInputManager.isComposable()); // 藍牙鍵盤打字時顯示候選欄 - return ret; - } - return textInputManager.isComposable() && !Rime.isVoidKeycode(keyCode); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - Timber.i("\t\tonKeyDown()\tkeycode=%d, event=%s", keyCode, event.toString()); - if (composeEvent(event) && onKeyEvent(event) && isWindowShown) return true; - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - Timber.i("\t\tonKeyUp()\tkeycode=%d, event=%s", keyCode, event.toString()); - if (composeEvent(event) && textInputManager.getNeedSendUpRimeKey()) { - textInputManager.onRelease(keyCode); - if (isWindowShown) return true; - } - return super.onKeyUp(keyCode, event); - } - - /** - * 處理實體鍵盤事件 - * - * @param event {@link KeyEvent 按鍵事件} - * @return 是否成功處理 - */ - // KeyEvent 处理实体键盘事件 - private boolean onKeyEvent(@NonNull KeyEvent event) { - Timber.i("\t\tonKeyEvent()\tRealKeyboard event=%s", event.toString()); - int keyCode = event.getKeyCode(); - textInputManager.setNeedSendUpRimeKey(Rime.isComposing()); - if (!isComposing()) { - if (keyCode == KeyEvent.KEYCODE_DEL - || keyCode == KeyEvent.KEYCODE_ENTER - || keyCode == KeyEvent.KEYCODE_ESCAPE - || keyCode == KeyEvent.KEYCODE_BACK) { - return false; - } - } else if (keyCode == KeyEvent.KEYCODE_BACK) { - keyCode = KeyEvent.KEYCODE_ESCAPE; // 返回鍵清屏 - } - if (event.getAction() == KeyEvent.ACTION_DOWN - && event.isCtrlPressed() - && event.getRepeatCount() == 0 - && !KeyEvent.isModifierKey(keyCode)) { - if (hookKeyboard(keyCode, event.getMetaState())) return true; - } - - final int unicodeChar = event.getUnicodeChar(); - final String s = String.valueOf((char) unicodeChar); - final int i = Event.getClickCode(s); - int mask = 0; - if (i > 0) { - keyCode = i; - } else { // 空格、回車等 - mask = event.getMetaState(); - } - final boolean ret = handleKey(keyCode, mask); - if (isComposing()) setCandidatesViewShown(textInputManager.isComposable()); // 藍牙鍵盤打字時顯示候選欄 - return ret; - } - - public void switchToPrevIme() { - try { - if (VERSION.SDK_INT >= VERSION_CODES.P) { - switchToPreviousInputMethod(); - } else { - Window window = getWindow().getWindow(); - if (window != null) { - SystemServicesKt.getInputMethodManager() - .switchToLastInputMethod(window.getAttributes().token); - } - } - } catch (Exception e) { - Timber.e(e, "Unable to switch to the previous IME."); - SystemServicesKt.getInputMethodManager().showInputMethodPicker(); - } - } - - public void switchToNextIme() { - try { - if (VERSION.SDK_INT >= VERSION_CODES.P) { - switchToNextInputMethod(false); - } else { - Window window = getWindow().getWindow(); - if (window != null) { - SystemServicesKt.getInputMethodManager() - .switchToNextInputMethod(window.getAttributes().token, false); - } - } - } catch (Exception e) { - Timber.e(e, "Unable to switch to the next IME."); - SystemServicesKt.getInputMethodManager().showInputMethodPicker(); - } - } - - // 处理键盘事件(Android keycode) - public boolean handleKey(int keyEventCode, int metaState) { // 軟鍵盤 - textInputManager.setNeedSendUpRimeKey(false); - if (onRimeKey(Event.getRimeEvent(keyEventCode, metaState))) { - // 如果输入法消费了按键事件,则需要释放按键 - textInputManager.setNeedSendUpRimeKey(true); - Timber.d( - "\t\thandleKey()\trimeProcess, keycode=%d, metaState=%d", - keyEventCode, metaState); - } else if (hookKeyboard(keyEventCode, metaState)) { - Timber.d("\t\thandleKey()\thookKeyboard, keycode=%d", keyEventCode); - } else if (performEnter(keyEventCode) || handleBack(keyEventCode)) { - // 处理返回键(隐藏软键盘)和回车键(换行) - // todo 确认是否有必要单独处理回车键?是否需要把back和escape全部占用? - Timber.d("\t\thandleKey()\tEnterOrHide, keycode=%d", keyEventCode); - } else if (ShortcutUtils.INSTANCE.openCategory(keyEventCode)) { - // 打开系统默认应用 - Timber.d("\t\thandleKey()\topenCategory keycode=%d", keyEventCode); - } else { - textInputManager.setNeedSendUpRimeKey(true); - Timber.d( - "\t\thandleKey()\treturn FALSE, keycode=%d, metaState=%d", - keyEventCode, metaState); - return false; - } - return true; - } - - public boolean shareText() { - if (VERSION.SDK_INT >= VERSION_CODES.M) { - final @Nullable InputConnection ic = getCurrentInputConnection(); - if (ic == null) return false; - CharSequence cs = ic.getSelectedText(0); - if (cs == null) ic.performContextMenuAction(android.R.id.selectAll); - return ic.performContextMenuAction(android.R.id.shareText); - } - return false; - } - - private boolean hookKeyboard(int code, int mask) { // 編輯操作 - final @Nullable InputConnection ic = getCurrentInputConnection(); - if (ic == null) return false; - if (mask == KeyEvent.META_CTRL_ON) { - - if (VERSION.SDK_INT >= VERSION_CODES.M) { - if (getPrefs().getKeyboard().getHookCtrlZY()) { - switch (code) { - case KeyEvent.KEYCODE_Y: - return ic.performContextMenuAction(android.R.id.redo); - case KeyEvent.KEYCODE_Z: - return ic.performContextMenuAction(android.R.id.undo); - } - } - } - switch (code) { - case KeyEvent.KEYCODE_A: - if (getPrefs().getKeyboard().getHookCtrlA()) - return ic.performContextMenuAction(android.R.id.selectAll); - return false; - case KeyEvent.KEYCODE_X: - if (getPrefs().getKeyboard().getHookCtrlCV()) { - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; - ExtractedText et = ic.getExtractedText(etr, 0); - if (et != null) { - if (et.selectionEnd - et.selectionStart > 0) - return ic.performContextMenuAction(android.R.id.cut); - } - } - Timber.i("hookKeyboard cut fail"); - return false; - case KeyEvent.KEYCODE_C: - if (getPrefs().getKeyboard().getHookCtrlCV()) { - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; - ExtractedText et = ic.getExtractedText(etr, 0); - if (et != null) { - if (et.selectionEnd - et.selectionStart > 0) - return ic.performContextMenuAction(android.R.id.copy); - } - } - Timber.i("hookKeyboard copy fail"); - return false; - case KeyEvent.KEYCODE_V: - if (getPrefs().getKeyboard().getHookCtrlCV()) { - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; - ExtractedText et = ic.getExtractedText(etr, 0); - if (et == null) { - Timber.d("hookKeyboard paste, et == null, try commitText"); - if (ic.commitText(ShortcutUtils.pasteFromClipboard(this), 1)) { - return true; - } - } else if (ic.performContextMenuAction(android.R.id.paste)) { - return true; - } - Timber.w("hookKeyboard paste fail"); - } - return false; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (getPrefs().getKeyboard().getHookCtrlLR()) { - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; - ExtractedText et = ic.getExtractedText(etr, 0); - if (et != null) { - int move_to = StringUtils.findSectionAfter(et.text, et.startOffset + et.selectionEnd); - ic.setSelection(move_to, move_to); - return true; - } - break; - } - case KeyEvent.KEYCODE_DPAD_LEFT: - if (getPrefs().getKeyboard().getHookCtrlLR()) { - ExtractedTextRequest etr = new ExtractedTextRequest(); - etr.token = 0; - ExtractedText et = ic.getExtractedText(etr, 0); - if (et != null) { - int move_to = - StringUtils.findSectionBefore(et.text, et.startOffset + et.selectionStart); - ic.setSelection(move_to, move_to); - return true; - } - break; - } - } - } - return false; - } - - /** 更新Rime的中西文狀態、編輯區文本 */ - public int updateComposing() { - final @Nullable InputConnection ic = getCurrentInputConnection(); - activeEditorInstance.updateComposingText(); - if (ic != null && !mCompositionPopupWindow.isWinFixed()) { - mCompositionPopupWindow.setCursorUpdated(ic.requestCursorUpdates(1)); - } - int startNum = 0; - if (mCandidateRoot != null) { - if (mCompositionPopupWindow.isPopupWindowEnabled()) { - Timber.d("updateComposing() SymbolKeyboardType=%s", symbolKeyboardType.toString()); - if (symbolKeyboardType != SymbolKeyboardType.NO_KEY - && symbolKeyboardType != SymbolKeyboardType.CANDIDATE) { - showLiquidKeyboardToolbar(); - } else { - mComposition.setVisibility(View.VISIBLE); - startNum = mComposition.setWindow(minPopupSize, minPopupCheckSize, Integer.MAX_VALUE); - mCandidate.setText(startNum); - // if isCursorUpdated, showCompositionView will be called in onUpdateCursorAnchorInfo - // otherwise we need to call it here - if (!mCompositionPopupWindow.isCursorUpdated()) showCompositionView(true); - } - } else { - mCandidate.setText(0); - } - mCandidate.setExpectWidth(mainKeyboardView.getWidth()); - // 刷新候选词后,如果候选词超出屏幕宽度,滚动候选栏 - mTabRoot.move(mCandidate.getHighlightLeft(), mCandidate.getHighlightRight()); - } - if (mainKeyboardView != null) mainKeyboardView.invalidateComposingKeys(); - if (!onEvaluateInputViewShown()) - setCandidatesViewShown(textInputManager.isComposable()); // 實體鍵盤打字時顯示候選欄 - - return startNum; - } - - private void showLiquidKeyboardToolbar() { - mComposition.changeToLiquidKeyboardToolbar(); - showCompositionView(false); - } - - /** - * 如果爲{@link KeyEvent#KEYCODE_ENTER 回車鍵},則換行 - * - * @param keyCode {@link KeyEvent#getKeyCode() 鍵碼} - * @return 是否處理了回車事件 - */ - private boolean performEnter(int keyCode) { // 回車 - if (keyCode == KeyEvent.KEYCODE_ENTER) { - DraftHelper.INSTANCE.onInputEventChanged(); - handleReturnKey(); - return true; - } - return false; - } - - /** 模擬PC鍵盤中Esc鍵的功能:清除輸入的編碼和候選項 */ - public void performEscape() { - if (isComposing()) textInputManager.onKey(KeyEvent.KEYCODE_ESCAPE, 0); - } - - @Override - public boolean onEvaluateFullscreenMode() { - final Configuration config = getResources().getConfiguration(); - if (config != null) { - if (config.orientation != Configuration.ORIENTATION_LANDSCAPE) { - return false; - } else { - switch (getPrefs().getKeyboard().getFullscreenMode()) { - case AUTO_SHOW: - Timber.d("FullScreen: Auto"); - final EditorInfo ei = getCurrentInputEditorInfo(); - if (ei != null && (ei.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) { - return false; - } - case ALWAYS_SHOW: - Timber.d("FullScreen: Always"); - return true; - case NEVER_SHOW: - Timber.d("FullScreen: Never"); - return false; - } - } - } - return false; - } - - @Override - public void updateFullscreenMode() { - super.updateFullscreenMode(); - updateSoftInputWindowLayoutParameters(); - } - - /** Updates the layout params of the window and input view. */ - private void updateSoftInputWindowLayoutParameters() { - final Window w = getWindow().getWindow(); - if (w == null) return; - if (inputView != null) { - final int layoutHeight = - isFullscreenMode() - ? WindowManager.LayoutParams.WRAP_CONTENT - : WindowManager.LayoutParams.MATCH_PARENT; - final View inputArea = w.findViewById(android.R.id.inputArea); - // TODO: 需要获取到文本编辑框、完成按钮,设置其色彩和尺寸。 - // if (isFullscreenMode()) { - // Timber.d("isFullscreenMode"); - // /* In Fullscreen mode, when layout contains transparent color, - // * the background under input area will disturb users' typing, - // * so set the input area as light pink */ - // inputArea.setBackgroundColor(parseColor("#ff660000")); - // } else { - // Timber.d("NotFullscreenMode"); - // /* Otherwise, set it as light gray to avoid potential issue */ - // inputArea.setBackgroundColor(parseColor("#dddddddd")); - // } - - ViewUtils.updateLayoutHeightOf(inputArea, layoutHeight); - ViewUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); - ViewUtils.updateLayoutHeightOf(inputView, layoutHeight); - } - } - - public boolean addEventListener(@NonNull EventListener listener) { - return eventListeners.add(listener); - } - - public boolean removeEventListener(@NonNull EventListener listener) { - return eventListeners.remove(listener); - } - - public interface EventListener { - default void onCreate() {} - - default void onInitializeInputUi(@NonNull InputView inputView) {} - - default void onDestroy() {} - - default void onStartInputView(@NonNull EditorInstance instance, boolean restarting) {} - - default void osFinishInputView(boolean finishingInput) {} - - default void onWindowShown() {} - - default void onWindowHidden() {} - - default void onUpdateSelection() {} - } - - private boolean candidateExPage = false; - - public boolean hasCandidateExPage() { - return candidateExPage; - } - - public void setCandidateExPage(boolean candidateExPage) { - this.candidateExPage = candidateExPage; - } -} diff --git a/app/src/main/java/com/osfans/trime/ime/core/Trime.kt b/app/src/main/java/com/osfans/trime/ime/core/Trime.kt new file mode 100644 index 0000000000..ec33f611a8 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/core/Trime.kt @@ -0,0 +1,1283 @@ +/* + * Copyright (C) 2015-present, osfans + * waxaca@163.com https://github.com/osfans + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.osfans.trime.ime.core + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.inputmethodservice.InputMethodService +import android.os.Build +import android.os.Build.VERSION_CODES +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.text.InputType +import android.text.TextUtils +import android.view.Gravity +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import android.view.inputmethod.CursorAnchorInfo +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.ExtractedTextRequest +import android.widget.FrameLayout +import com.osfans.trime.BuildConfig +import com.osfans.trime.R +import com.osfans.trime.core.Rime +import com.osfans.trime.core.Rime.Companion.compositionText +import com.osfans.trime.core.Rime.Companion.getOption +import com.osfans.trime.core.Rime.Companion.isAsciiMode +import com.osfans.trime.core.Rime.Companion.isAsciiPunch +import com.osfans.trime.core.Rime.Companion.isEmpty +import com.osfans.trime.core.Rime.Companion.isVoidKeycode +import com.osfans.trime.core.Rime.Companion.processKey +import com.osfans.trime.core.Rime.Companion.setCaretPos +import com.osfans.trime.core.Rime.Companion.setOption +import com.osfans.trime.data.AppPrefs +import com.osfans.trime.data.AppPrefs.Companion.defaultInstance +import com.osfans.trime.data.db.DraftHelper.onInputEventChanged +import com.osfans.trime.data.sound.SoundThemeManager.switchSound +import com.osfans.trime.data.theme.Theme.Companion.get +import com.osfans.trime.databinding.CompositionRootBinding +import com.osfans.trime.ime.broadcast.IntentReceiver +import com.osfans.trime.ime.core.RimeWrapper.isReady +import com.osfans.trime.ime.core.RimeWrapper.runAfterStarted +import com.osfans.trime.ime.core.RimeWrapper.runCheck +import com.osfans.trime.ime.core.RimeWrapper.startup +import com.osfans.trime.ime.enums.Keycode.Companion.isStdKey +import com.osfans.trime.ime.enums.SymbolKeyboardType +import com.osfans.trime.ime.keyboard.Event.Companion.getClickCode +import com.osfans.trime.ime.keyboard.Event.Companion.getRimeEvent +import com.osfans.trime.ime.keyboard.InitializationUi +import com.osfans.trime.ime.keyboard.InputFeedbackManager +import com.osfans.trime.ime.keyboard.Key.Companion.isTrimeModifierKey +import com.osfans.trime.ime.keyboard.KeyboardSwitcher.currentKeyboard +import com.osfans.trime.ime.keyboard.KeyboardSwitcher.newOrReset +import com.osfans.trime.ime.keyboard.KeyboardView +import com.osfans.trime.ime.keyboard.KeyboardWindow +import com.osfans.trime.ime.landscapeinput.LandscapeInputUIMode +import com.osfans.trime.ime.lifecycle.LifecycleInputMethodService +import com.osfans.trime.ime.symbol.LiquidKeyboard +import com.osfans.trime.ime.symbol.TabManager +import com.osfans.trime.ime.symbol.TabView +import com.osfans.trime.ime.text.Candidate +import com.osfans.trime.ime.text.Composition +import com.osfans.trime.ime.text.CompositionPopupWindow +import com.osfans.trime.ime.text.ScrollView +import com.osfans.trime.ime.text.TextInputManager +import com.osfans.trime.ime.util.UiUtil.isDarkMode +import com.osfans.trime.util.ShortcutUtils.openCategory +import com.osfans.trime.util.ShortcutUtils.pasteFromClipboard +import com.osfans.trime.util.ShortcutUtils.syncInBackground +import com.osfans.trime.util.StringUtils.findSectionAfter +import com.osfans.trime.util.StringUtils.findSectionBefore +import com.osfans.trime.util.ViewUtils.updateLayoutGravityOf +import com.osfans.trime.util.ViewUtils.updateLayoutHeightOf +import com.osfans.trime.util.WeakHashSet +import com.osfans.trime.util.dp2px +import splitties.bitflags.hasFlag +import splitties.systemservices.inputMethodManager +import timber.log.Timber +import java.util.Objects + +/** [輸入法][InputMethodService]主程序 */ + +@Suppress("ktlint:standard:property-naming") +open class Trime : LifecycleInputMethodService() { + private var liquidKeyboard: LiquidKeyboard? = null + private var normalTextEditor = false + private val prefs: AppPrefs + get() = defaultInstance() + private var darkMode = false // 当前键盘主题是否处于暗黑模式 + private var mainKeyboardView: KeyboardView? = null // 主軟鍵盤 + private var mCandidate: Candidate? = null // 候選 + private var mComposition: Composition? = null // 編碼 + private var compositionRootBinding: CompositionRootBinding? = null + private var mCandidateRoot: ScrollView? = null + private var mTabRoot: ScrollView? = null + private var tabView: TabView? = null + var inputView: InputView? = null + private var eventListeners = WeakHashSet() + var inputFeedbackManager: InputFeedbackManager? = null // 效果管理器 + private var mIntentReceiver: IntentReceiver? = null + private var editorInfo: EditorInfo? = null + private var isWindowShown = false // 键盘窗口是否已显示 + private var isAutoCaps = false // 句首自動大寫 + var activeEditorInstance: EditorInstance? = null + var textInputManager: TextInputManager? = null // 文字输入管理器 + private var minPopupSize = 0 // 上悬浮窗的候选词的最小词长 + private var minPopupCheckSize = 0 // 第一屏候选词数量少于设定值,则候选词上悬浮窗。(也就是说,第一屏存在长词)此选项大于1时,min_length等参数失效 + private var mCompositionPopupWindow: CompositionPopupWindow? = null + private var candidateExPage = false + + fun hasCandidateExPage(): Boolean { + return candidateExPage + } + + fun setCandidateExPage(value: Boolean) { + candidateExPage = value + } + + init { + try { + check(self == null) { "Trime is already initialized" } + self = this + } catch (e: Exception) { + Timber.e(e) + } + } + + override fun onWindowShown() { + super.onWindowShown() + if (isWindowShown) { + Timber.i("Ignoring (is already shown)") + return + } else { + Timber.i("onWindowShown...") + } + if (isReady() && activeEditorInstance != null) { + isWindowShown = true + updateComposing() + for (listener in eventListeners) { + listener.onWindowShown() + } + } + } + + override fun onWindowHidden() { + super.onWindowHidden() + if (!isWindowShown) { + Timber.d("Ignoring (window is already hidden)") + return + } else { + Timber.d("onWindowHidden") + } + isWindowShown = false + if (prefs.profile.syncBackgroundEnabled) { + val msg = Message() + msg.obj = this + syncBackgroundHandler.sendMessageDelayed(msg, 5000) // 输入面板隐藏5秒后,开始后台同步 + } + for (listener in eventListeners) { + listener.onWindowHidden() + } + } + + fun updatePopupWindow( + offsetX: Int, + offsetY: Int, + ) { + mCompositionPopupWindow!!.updatePopupWindow(offsetX, offsetY) + } + + fun loadConfig() { + val theme = get() + minPopupSize = theme.style.getInt("layout/min_length") + minPopupCheckSize = theme.style.getInt("layout/min_check") + textInputManager!!.shouldResetAsciiMode = theme.style.getBoolean("reset_ascii_mode") + isAutoCaps = theme.style.getBoolean("auto_caps") + textInputManager!!.shouldUpdateRimeOption = true + mCompositionPopupWindow!!.loadConfig(theme, prefs) + } + + private fun updateRimeOption(): Boolean { + try { + if (textInputManager!!.shouldUpdateRimeOption) { + setOption("soft_cursor", prefs.keyboard.softCursorEnabled) // 軟光標 + setOption("_horizontal", get().style.getBoolean("horizontal")) // 水平模式 + textInputManager!!.shouldUpdateRimeOption = false + } + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + /** 防止重启系统 强行停止应用时alarm任务丢失 */ + @SuppressLint("ScheduleExactAlarm") + fun restartSystemStartTimingSync() { + if (prefs.profile.timingSyncEnabled) { + val triggerTime = prefs.profile.timingSyncTriggerTime + val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager + + /** 设置待发送的同步事件 */ + val pendingIntent = + PendingIntent.getBroadcast( + this, + 0, + Intent("com.osfans.trime.timing.sync"), + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + }, + ) + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { // 根据SDK设置alarm任务 + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) + } else { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) + } + } + } + + override fun onCreate() { + super.onCreate() + // MUST WRAP all code within Service onCreate() in try..catch to prevent any crash loops + try { + // Additional try..catch wrapper as the event listeners chain or the super.onCreate() method + // could crash + // and lead to a crash loop + Timber.d("onCreate") + val context: InputMethodService = this + startup { + Timber.d("Running Trime.onCreate") + textInputManager = TextInputManager.getInstance(isDarkMode(context)) + activeEditorInstance = EditorInstance(context) + inputFeedbackManager = InputFeedbackManager(context) + liquidKeyboard = LiquidKeyboard(context) + mCompositionPopupWindow = CompositionPopupWindow() + restartSystemStartTimingSync() + try { + for (listener in eventListeners) { + listener.onCreate() + } + } catch (e: Exception) { + Timber.e(e) + } + Timber.d("Trime.onCreate completed") + } + } catch (e: Exception) { + Timber.e(e) + } + } + + /** + * 变更配置的暗黑模式开关 + * + * @param darkMode 设置为暗黑模式 + * @return 模式实际上是否有发生变更 + */ + private fun setDarkMode(darkMode: Boolean): Boolean { + if (darkMode != this.darkMode) { + Timber.d("Dark mode changed: %s", darkMode) + this.darkMode = darkMode + return true + } + Timber.d("Dark mode not changed: %s", darkMode) + return false + } + + private var symbolKeyboardType = SymbolKeyboardType.NO_KEY + + fun inputSymbol(text: String) { + textInputManager!!.onPress(KeyEvent.KEYCODE_UNKNOWN) + if (isAsciiMode) setOption("ascii_mode", false) + val asciiPunch = isAsciiPunch + if (asciiPunch) setOption("ascii_punct", false) + textInputManager!!.onText("{Escape}$text") + if (asciiPunch) setOption("ascii_punct", true) + self!!.selectLiquidKeyboard(-1) + } + + fun selectLiquidKeyboard(tabIndex: Int) { + if (inputView == null) return + if (tabIndex >= 0) { + inputView!!.switchUiByState(KeyboardWindow.State.Symbol) + symbolKeyboardType = liquidKeyboard!!.select(tabIndex) + tabView!!.updateTabWidth() + mTabRoot!!.background = mCandidateRoot!!.background + mTabRoot!!.move(tabView!!.hightlightLeft, tabView!!.hightlightRight) + showLiquidKeyboardToolbar() + } else { + symbolKeyboardType = SymbolKeyboardType.NO_KEY + // 设置液体键盘处于隐藏状态 + TabManager.get().setTabExited() + inputView!!.switchUiByState(KeyboardWindow.State.Main) + updateComposing() + } + } + + // 按键需要通过tab name来打开liquidKeyboard的指定tab + fun selectLiquidKeyboard(name: String) { + if (name.matches("-?\\d+".toRegex())) { + selectLiquidKeyboard(name.toInt()) + } else if (name.matches("[A-Z]+".toRegex())) { + selectLiquidKeyboard(SymbolKeyboardType.valueOf(name)) + } else { + selectLiquidKeyboard(TabManager.getTagIndex(name)) + } + } + + fun selectLiquidKeyboard(type: SymbolKeyboardType?) { + selectLiquidKeyboard(TabManager.getTagIndex(type)) + } + + fun pasteByChar() { + commitTextByChar(Objects.requireNonNull(pasteFromClipboard(this)).toString()) + } + + fun invalidate() { + Rime.getInstance(false) + get().destroy() + reset() + textInputManager!!.shouldUpdateRimeOption = true + } + + private fun showCompositionView(isCandidate: Boolean) { + if (TextUtils.isEmpty(compositionText) && isCandidate) { + mCompositionPopupWindow!!.hideCompositionView() + return + } + compositionRootBinding!!.compositionRoot.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + mCompositionPopupWindow!!.updateCompositionView( + compositionRootBinding!!.compositionRoot.measuredWidth, + compositionRootBinding!!.compositionRoot.measuredHeight, + ) + } + + private fun loadBackground() { + val theme = get() + val orientation = resources.configuration.orientation + val textBackground = + theme.colors.getDrawable( + "text_back_color", + "layout/border", + "border_color", + "layout/round_corner", + "layout/alpha", + ) + mCompositionPopupWindow!!.setThemeStyle( + dp2px(theme.style.getFloat("layout/elevation")).toInt().toFloat(), + textBackground, + ) + if (mCandidateRoot != null) { + val candidateBackground = + theme.colors.getDrawable( + "candidate_background", + "candidate_border", + "candidate_border_color", + "candidate_border_round", + null, + ) + if (candidateBackground != null) mCandidateRoot!!.background = candidateBackground + } + if (inputView == null) return + + // 单手键盘模式 + val oneHandMode = 0 + val padding = theme.getKeyboardPadding(oneHandMode, orientation == Configuration.ORIENTATION_LANDSCAPE) + Timber.i( + "update KeyboardPadding: Trime.loadBackground, padding= %s %s %s, orientation=%s", + padding[0], + padding[1], + padding[2], + orientation, + ) + mainKeyboardView!!.setPadding(padding[0], 0, padding[1], padding[2]) + val inputRootBackground = theme.colors.getDrawable("root_background") + if (inputRootBackground != null) { + inputView!!.keyboardView.background = inputRootBackground + } else { + // 避免因为键盘整体透明而造成的异常 + inputView!!.keyboardView.setBackgroundColor(Color.BLACK) + } + tabView!!.reset() + } + + fun resetKeyboard() { + if (mainKeyboardView != null) { + mainKeyboardView!!.setShowHint(!getOption("_hide_key_hint")) + mainKeyboardView!!.setShowSymbol(!getOption("_hide_key_symbol")) + mainKeyboardView!!.reset() // 實體鍵盤無軟鍵盤 + } + } + + fun resetCandidate() { + if (mCandidateRoot != null) { + loadBackground() + setShowComment(!getOption("_hide_comment")) + mCandidateRoot!!.visibility = if (!getOption("_hide_candidate")) View.VISIBLE else View.GONE + mCandidate!!.reset() + mCompositionPopupWindow!!.isPopupWindowEnabled = ( + prefs.keyboard.popupWindowEnabled && + get().style.getObject("window") != null + ) + mComposition!!.visibility = if (mCompositionPopupWindow!!.isPopupWindowEnabled) View.VISIBLE else View.GONE + mComposition!!.reset() + } + } + + /** 重置鍵盤、候選條、狀態欄等 !!注意,如果其中調用Rime.setOption,切換方案會卡住 */ + private fun reset() { + if (inputView == null) return + inputView!!.switchUiByState(KeyboardWindow.State.Main) + loadConfig() + updateDarkMode() + val theme = get(darkMode) + theme.initCurrentColors(darkMode) + switchSound(theme.colors.getString("sound")) + newOrReset() + resetCandidate() + mCompositionPopupWindow!!.hideCompositionView() + resetKeyboard() + } + + /** Must be called on the UI thread */ + fun initKeyboard() { + if (textInputManager != null) { + reset() + // setNavBarColor(); + textInputManager!!.shouldUpdateRimeOption = true // 不能在Rime.onMessage中調用set_option,會卡死 + bindKeyboardToInputView() + // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground(); + updateComposing() // 切換主題時刷新候選 + } + } + + private fun initKeyboardDarkMode(darkMode: Boolean) { + val theme = get() + if (theme.hasDarkLight) { + loadConfig() + theme.initCurrentColors(darkMode) + switchSound(theme.colors.getString("sound")) + newOrReset() + resetCandidate() + mCompositionPopupWindow!!.hideCompositionView() + resetKeyboard() + + // setNavBarColor(); + textInputManager!!.shouldUpdateRimeOption = true // 不能在Rime.onMessage中調用set_option,會卡死 + bindKeyboardToInputView() + // loadBackground(); // reset()调用过resetCandidate(),resetCandidate()一键调用过loadBackground(); + updateComposing() // 切換主題時刷新候選 + } + } + + override fun onDestroy() { + if (mIntentReceiver != null) mIntentReceiver!!.unregisterReceiver(this) + mIntentReceiver = null + if (inputFeedbackManager != null) inputFeedbackManager!!.destroy() + inputFeedbackManager = null + inputView = null + for (listener in eventListeners) { + listener.onDestroy() + } + eventListeners.clear() + if (mCompositionPopupWindow != null) { + mCompositionPopupWindow!!.destroy() + } + super.onDestroy() + self = null + } + + private fun handleReturnKey() { + if (editorInfo == null) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) + return + } + if (editorInfo!!.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_NULL) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) + return + } + if (editorInfo!!.imeOptions.hasFlag(EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + val ic = currentInputConnection + ic?.commitText("\n", 1) + return + } + if (!TextUtils.isEmpty(editorInfo!!.actionLabel) && + editorInfo!!.actionId != EditorInfo.IME_ACTION_UNSPECIFIED + ) { + val ic = currentInputConnection + ic?.performEditorAction(editorInfo!!.actionId) + return + } + val action = editorInfo!!.imeOptions and EditorInfo.IME_MASK_ACTION + val ic = currentInputConnection + when (action) { + EditorInfo.IME_ACTION_UNSPECIFIED, EditorInfo.IME_ACTION_NONE -> ic?.commitText("\n", 1) + else -> ic?.performEditorAction(action) + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + val config = resources.configuration + if (config != null) { + if (config.orientation != newConfig.orientation) { + // Clear composing text and candidates for orientation change. + performEscape() + config.orientation = newConfig.orientation + } + } + super.onConfigurationChanged(newConfig) + } + + override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) { + mCompositionPopupWindow!!.updateCursorAnchorInfo(cursorAnchorInfo) + if (mCandidateRoot != null) { + showCompositionView(true) + } + } + + override fun onUpdateSelection( + oldSelStart: Int, + oldSelEnd: Int, + newSelStart: Int, + newSelEnd: Int, + candidatesStart: Int, + candidatesEnd: Int, + ) { + super.onUpdateSelection( + oldSelStart, + oldSelEnd, + newSelStart, + newSelEnd, + candidatesStart, + candidatesEnd, + ) + if (candidatesEnd != -1 && (newSelStart != candidatesEnd || newSelEnd != candidatesEnd)) { + // 移動光標時,更新候選區 + if (newSelEnd in candidatesStart..(android.R.id.inputArea) + val lP1 = inputArea.layoutParams + lP1.height = ViewGroup.LayoutParams.MATCH_PARENT + inputArea.layoutParams = lP1 + super.setInputView(view) + val lP2 = view.layoutParams + lP2.height = ViewGroup.LayoutParams.MATCH_PARENT + view.layoutParams = lP2 + } + + override fun onConfigureWindow( + win: Window, + isFullscreen: Boolean, + isCandidatesOnly: Boolean, + ) { + win.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + fun setShowComment(showComment: Boolean) { + if (mCandidateRoot != null) mCandidate!!.setShowComment(showComment) + mComposition!!.setShowComment(showComment) + } + + override fun onStartInput( + attribute: EditorInfo, + restarting: Boolean, + ) { + editorInfo = attribute + Timber.d("onStartInput: restarting=%s", restarting) + } + + private fun updateDarkMode(): Boolean { + val isDarkMode = isDarkMode(this) + return setDarkMode(isDarkMode) + } + + override fun onStartInputView( + attribute: EditorInfo, + restarting: Boolean, + ) { + Timber.d("onStartInputView: restarting=%s", restarting) + editorInfo = attribute + runAfterStarted { + if (updateDarkMode()) { + initKeyboardDarkMode(darkMode) + } + inputFeedbackManager!!.resumeSoundPool() + inputFeedbackManager!!.resetPlayProgress() + for (listener in eventListeners) { + listener.onStartInputView(activeEditorInstance!!, restarting) + } + if (prefs.other.showStatusBarIcon) { + showStatusIcon(R.drawable.ic_trime_status) // 狀態欄圖標 + } + bindKeyboardToInputView() + // if (!restarting) setNavBarColor(); + setCandidatesViewShown(!isEmpty) // 軟鍵盤出現時顯示候選欄 + if (attribute.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION + == EditorInfo.IME_FLAG_NO_ENTER_ACTION + ) { + mainKeyboardView!!.resetEnterLabel() + } else { + mainKeyboardView!!.setEnterLabel( + attribute.imeOptions and EditorInfo.IME_MASK_ACTION, + attribute.actionLabel, + ) + } + when (attribute.inputType and InputType.TYPE_MASK_VARIATION) { + InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, + InputType.TYPE_TEXT_VARIATION_PASSWORD, + InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, + InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS, + InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD, + -> { + Timber.i( + "EditorInfo: private;" + + " packageName=" + + attribute.packageName + + "; fieldName=" + + attribute.fieldName + + "; actionLabel=" + + attribute.actionLabel + + "; inputType=" + + attribute.inputType + + "; VARIATION=" + + (attribute.inputType and InputType.TYPE_MASK_VARIATION) + + "; CLASS=" + + (attribute.inputType and InputType.TYPE_MASK_CLASS) + + "; ACTION=" + + (attribute.imeOptions and EditorInfo.IME_MASK_ACTION), + ) + normalTextEditor = false + } + + else -> { + Timber.i( + "EditorInfo: normal;" + + " packageName=" + + attribute.packageName + + "; fieldName=" + + attribute.fieldName + + "; actionLabel=" + + attribute.actionLabel + + "; inputType=" + + attribute.inputType + + "; VARIATION=" + + (attribute.inputType and InputType.TYPE_MASK_VARIATION) + + "; CLASS=" + + (attribute.inputType and InputType.TYPE_MASK_CLASS) + + "; ACTION=" + + (attribute.imeOptions and EditorInfo.IME_MASK_ACTION), + ) + if (attribute.imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + == EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + ) { + // 应用程求以隐身模式打开键盘应用程序 + normalTextEditor = false + Timber.d("EditorInfo: normal -> private, IME_FLAG_NO_PERSONALIZED_LEARNING") + } else if (attribute.packageName == BuildConfig.APPLICATION_ID || + prefs + .clipboard + .draftExcludeApp + .contains(attribute.packageName) + ) { + normalTextEditor = false + Timber.d("EditorInfo: normal -> exclude, packageName=%s", attribute.packageName) + } else { + normalTextEditor = true + onInputEventChanged() + } + } + } + } + runCheck() + } + + override fun onFinishInputView(finishingInput: Boolean) { + super.onFinishInputView(finishingInput) + if (isReady()) { + if (normalTextEditor) { + onInputEventChanged() + } + try { + // Dismiss any pop-ups when the input-view is being finished and hidden. + mainKeyboardView!!.closing() + performEscape() + if (inputFeedbackManager != null) { + inputFeedbackManager!!.releaseSoundPool() + } + mCompositionPopupWindow!!.hideCompositionView() + } catch (e: Exception) { + Timber.e(e, "Failed to show the PopupWindow.") + } + } + if (inputView != null) { + inputView!!.finishInput() + } + Timber.d("OnFinishInputView") + } + + override fun onFinishInput() { + editorInfo = null + super.onFinishInput() + } + + fun bindKeyboardToInputView() { + if (mainKeyboardView != null) { + // Bind the selected keyboard to the input view. + val sk = currentKeyboard + mainKeyboardView!!.keyboard = sk + dispatchCapsStateToInputView() + } + } + + /** + * Dispatches cursor caps info to input view in order to implement auto caps lock at the start of + * a sentence. + */ + private fun dispatchCapsStateToInputView() { + if (isAutoCaps && isAsciiMode && mainKeyboardView != null && !mainKeyboardView!!.isCapsOn) { + mainKeyboardView!!.setShifted(false, activeEditorInstance!!.cursorCapsMode != 0) + } + } + + private val isComposing: Boolean + get() = Rime.isComposing + + /** + * Commit the current composing text together with the new text + * + * @param text the new text to be committed + */ + fun commitText(text: String?) { + currentInputConnection.finishComposingText() + activeEditorInstance!!.commitText(text!!, true) + } + + private fun commitTextByChar(text: String) { + for (i in text.indices) { + if (!activeEditorInstance!!.commitText(text.substring(i, i + 1), false)) break + } + } + + /** + * 如果爲Back鍵[KeyEvent.KEYCODE_BACK],則隱藏鍵盤 + * + * @param keyCode 鍵碼[KeyEvent.getKeyCode] + * @return 是否處理了Back鍵事件 + */ + private fun handleBack(keyCode: Int): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + requestHideSelf(0) + return true + } + return false + } + + private fun onRimeKey(event: IntArray): Boolean { + updateRimeOption() + // todo 改为异步处理按键事件、刷新UI + val ret = processKey(event[0], event[1]) + activeEditorInstance!!.commitRimeText() + return ret + } + + private fun composeEvent(event: KeyEvent): Boolean { + if (textInputManager == null) { + return false + } + val keyCode = event.keyCode + if (keyCode == KeyEvent.KEYCODE_MENU) return false // 不處理 Menu 鍵 + if (!isStdKey(keyCode)) return false // 只處理安卓標準按鍵 + if (event.repeatCount == 0 && isTrimeModifierKey(keyCode)) { + val ret = + onRimeKey( + getRimeEvent( + keyCode, + if (event.action == KeyEvent.ACTION_DOWN) event.modifiers else Rime.META_RELEASE_ON, + ), + ) + if (this.isComposing) setCandidatesViewShown(textInputManager!!.isComposable) // 藍牙鍵盤打字時顯示候選欄 + return ret + } + return textInputManager!!.isComposable && !isVoidKeycode(keyCode) + } + + override fun onKeyDown( + keyCode: Int, + event: KeyEvent, + ): Boolean { + Timber.i("\t\tonKeyDown()\tkeycode=%d, event=%s", keyCode, event.toString()) + return if (composeEvent(event) && onKeyEvent(event) && isWindowShown) true else super.onKeyDown(keyCode, event) + } + + override fun onKeyUp( + keyCode: Int, + event: KeyEvent, + ): Boolean { + Timber.i("\t\tonKeyUp()\tkeycode=%d, event=%s", keyCode, event.toString()) + if (composeEvent(event) && textInputManager!!.needSendUpRimeKey) { + textInputManager!!.onRelease(keyCode) + if (isWindowShown) return true + } + return super.onKeyUp(keyCode, event) + } + + /** + * 处理实体键盘事件 + * + * @param event 按鍵事件[KeyEvent] + * @return 是否成功處理 + */ + private fun onKeyEvent(event: KeyEvent): Boolean { + Timber.i("\t\tonKeyEvent()\tRealKeyboard event=%s", event.toString()) + var keyCode = event.keyCode + textInputManager!!.needSendUpRimeKey = Rime.isComposing + if (!this.isComposing) { + if (keyCode == KeyEvent.KEYCODE_DEL || + keyCode == KeyEvent.KEYCODE_ENTER || + keyCode == KeyEvent.KEYCODE_ESCAPE || + keyCode == KeyEvent.KEYCODE_BACK + ) { + return false + } + } else if (keyCode == KeyEvent.KEYCODE_BACK) { + keyCode = KeyEvent.KEYCODE_ESCAPE // 返回鍵清屏 + } + if (event.action == KeyEvent.ACTION_DOWN && event.isCtrlPressed && event.repeatCount == 0 && !KeyEvent.isModifierKey(keyCode)) { + if (hookKeyboard(keyCode, event.metaState)) return true + } + val unicodeChar = event.unicodeChar + val s = unicodeChar.toChar().toString() + val i = getClickCode(s) + var mask = 0 + if (i > 0) { + keyCode = i + } else { // 空格、回車等 + mask = event.metaState + } + val ret = handleKey(keyCode, mask) + if (this.isComposing) setCandidatesViewShown(textInputManager!!.isComposable) // 藍牙鍵盤打字時顯示候選欄 + return ret + } + + fun switchToPrevIme() { + try { + if (Build.VERSION.SDK_INT >= VERSION_CODES.P) { + switchToPreviousInputMethod() + } else { + val window = window.window + if (window != null) { + inputMethodManager + .switchToLastInputMethod(window.attributes.token) + } + } + } catch (e: Exception) { + Timber.e(e, "Unable to switch to the previous IME.") + inputMethodManager.showInputMethodPicker() + } + } + + fun switchToNextIme() { + try { + if (Build.VERSION.SDK_INT >= VERSION_CODES.P) { + switchToNextInputMethod(false) + } else { + val window = window.window + if (window != null) { + inputMethodManager + .switchToNextInputMethod(window.attributes.token, false) + } + } + } catch (e: Exception) { + Timber.e(e, "Unable to switch to the next IME.") + inputMethodManager.showInputMethodPicker() + } + } + + // 处理键盘事件(Android keycode) + fun handleKey( + keyEventCode: Int, + metaState: Int, + ): Boolean { // 軟鍵盤 + textInputManager!!.needSendUpRimeKey = false + if (onRimeKey(getRimeEvent(keyEventCode, metaState))) { + // 如果输入法消费了按键事件,则需要释放按键 + textInputManager!!.needSendUpRimeKey = true + Timber.d( + "\t\thandleKey()\trimeProcess, keycode=%d, metaState=%d", + keyEventCode, + metaState, + ) + } else if (hookKeyboard(keyEventCode, metaState)) { + Timber.d("\t\thandleKey()\thookKeyboard, keycode=%d", keyEventCode) + } else if (performEnter(keyEventCode) || handleBack(keyEventCode)) { + // 处理返回键(隐藏软键盘)和回车键(换行) + // todo 确认是否有必要单独处理回车键?是否需要把back和escape全部占用? + Timber.d("\t\thandleKey()\tEnterOrHide, keycode=%d", keyEventCode) + } else if (openCategory(keyEventCode)) { + // 打开系统默认应用 + Timber.d("\t\thandleKey()\topenCategory keycode=%d", keyEventCode) + } else { + textInputManager!!.needSendUpRimeKey = true + Timber.d( + "\t\thandleKey()\treturn FALSE, keycode=%d, metaState=%d", + keyEventCode, + metaState, + ) + return false + } + return true + } + + fun shareText(): Boolean { + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { + val ic = currentInputConnection ?: return false + val cs = ic.getSelectedText(0) + if (cs == null) ic.performContextMenuAction(android.R.id.selectAll) + return ic.performContextMenuAction(android.R.id.shareText) + } + return false + } + + /** 編輯操作 */ + private fun hookKeyboard( + code: Int, + mask: Int, + ): Boolean { + val ic = currentInputConnection ?: return false + // 没按下 Ctrl 键 + if (mask != KeyEvent.META_CTRL_ON) { + return false + } + + if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { + if (prefs.keyboard.hookCtrlZY) { + when (code) { + KeyEvent.KEYCODE_Y -> return ic.performContextMenuAction(android.R.id.redo) + KeyEvent.KEYCODE_Z -> return ic.performContextMenuAction(android.R.id.undo) + } + } + } + + when (code) { + KeyEvent.KEYCODE_A -> { + // 全选 + return if (prefs.keyboard.hookCtrlA) ic.performContextMenuAction(android.R.id.selectAll) else false + } + + KeyEvent.KEYCODE_X -> { + // 剪切 + if (prefs.keyboard.hookCtrlCV) { + val etr = ExtractedTextRequest() + etr.token = 0 + val et = ic.getExtractedText(etr, 0) + if (et != null) { + if (et.selectionEnd - et.selectionStart > 0) return ic.performContextMenuAction(android.R.id.cut) + } + } + Timber.i("hookKeyboard cut fail") + return false + } + + KeyEvent.KEYCODE_C -> { + // 复制 + if (prefs.keyboard.hookCtrlCV) { + val etr = ExtractedTextRequest() + etr.token = 0 + val et = ic.getExtractedText(etr, 0) + if (et != null) { + if (et.selectionEnd - et.selectionStart > 0) return ic.performContextMenuAction(android.R.id.copy) + } + } + Timber.i("hookKeyboard copy fail") + return false + } + + KeyEvent.KEYCODE_V -> { + // 粘贴 + if (prefs.keyboard.hookCtrlCV) { + val etr = ExtractedTextRequest() + etr.token = 0 + val et = ic.getExtractedText(etr, 0) + if (et == null) { + Timber.d("hookKeyboard paste, et == null, try commitText") + if (ic.commitText(pasteFromClipboard(this), 1)) { + return true + } + } else if (ic.performContextMenuAction(android.R.id.paste)) { + return true + } + Timber.w("hookKeyboard paste fail") + } + return false + } + + KeyEvent.KEYCODE_DPAD_RIGHT -> { + if (prefs.keyboard.hookCtrlLR) { + val etr = ExtractedTextRequest() + etr.token = 0 + val et = ic.getExtractedText(etr, 0) + if (et != null) { + val moveTo = findSectionAfter(et.text, et.startOffset + et.selectionEnd) + ic.setSelection(moveTo, moveTo) + return true + } + } + } + + KeyEvent.KEYCODE_DPAD_LEFT -> + if (prefs.keyboard.hookCtrlLR) { + val etr = ExtractedTextRequest() + etr.token = 0 + val et = ic.getExtractedText(etr, 0) + if (et != null) { + val moveTo = findSectionBefore(et.text, et.startOffset + et.selectionStart) + ic.setSelection(moveTo, moveTo) + return true + } + } + } + return false + } + + /** 更新Rime的中西文狀態、編輯區文本 */ + fun updateComposing(): Int { + val ic = currentInputConnection + activeEditorInstance!!.updateComposingText() + if (ic != null && !mCompositionPopupWindow!!.isWinFixed()) { + mCompositionPopupWindow!!.isCursorUpdated = ic.requestCursorUpdates(1) + } + var startNum = 0 + if (mCandidateRoot != null) { + if (mCompositionPopupWindow!!.isPopupWindowEnabled) { + Timber.d("updateComposing() SymbolKeyboardType=%s", symbolKeyboardType.toString()) + if (symbolKeyboardType != SymbolKeyboardType.NO_KEY && + symbolKeyboardType != SymbolKeyboardType.CANDIDATE + ) { + showLiquidKeyboardToolbar() + } else { + mComposition!!.visibility = View.VISIBLE + startNum = mComposition!!.setWindow(minPopupSize, minPopupCheckSize, Int.MAX_VALUE) + mCandidate!!.setText(startNum) + // if isCursorUpdated, showCompositionView will be called in onUpdateCursorAnchorInfo + // otherwise we need to call it here + if (!mCompositionPopupWindow!!.isCursorUpdated) showCompositionView(true) + } + } else { + mCandidate!!.setText(0) + } + mCandidate!!.setExpectWidth(mainKeyboardView!!.width) + // 刷新候选词后,如果候选词超出屏幕宽度,滚动候选栏 + mTabRoot!!.move(mCandidate!!.highlightLeft, mCandidate!!.highlightRight) + } + if (mainKeyboardView != null) mainKeyboardView!!.invalidateComposingKeys() + if (!onEvaluateInputViewShown()) setCandidatesViewShown(textInputManager!!.isComposable) // 實體鍵盤打字時顯示候選欄 + return startNum + } + + private fun showLiquidKeyboardToolbar() { + mComposition!!.changeToLiquidKeyboardToolbar() + showCompositionView(false) + } + + /** + * 如果爲回車鍵[KeyEvent.KEYCODE_ENTER],則換行 + * + * @param keyCode 鍵碼[KeyEvent.getKeyCode] + * @return 是否處理了回車事件 + */ + private fun performEnter(keyCode: Int): Boolean { // 回車 + if (keyCode == KeyEvent.KEYCODE_ENTER) { + onInputEventChanged() + handleReturnKey() + return true + } + return false + } + + /** 模擬PC鍵盤中Esc鍵的功能:清除輸入的編碼和候選項 */ + fun performEscape() { + if (this.isComposing) textInputManager!!.onKey(KeyEvent.KEYCODE_ESCAPE, 0) + } + + override fun onEvaluateFullscreenMode(): Boolean { + val config = resources.configuration + if (config == null || config.orientation != Configuration.ORIENTATION_LANDSCAPE) return false + return when (prefs.keyboard.fullscreenMode) { + LandscapeInputUIMode.AUTO_SHOW -> { + Timber.d("FullScreen: Auto") + val ei = currentInputEditorInfo + if (ei != null && ei.imeOptions and EditorInfo.IME_FLAG_NO_FULLSCREEN != 0) { + return false + } + Timber.d("FullScreen: Always") + true + } + + LandscapeInputUIMode.ALWAYS_SHOW -> { + Timber.d("FullScreen: Always") + true + } + + LandscapeInputUIMode.NEVER_SHOW -> { + Timber.d("FullScreen: Never") + false + } + } + } + + override fun updateFullscreenMode() { + super.updateFullscreenMode() + updateSoftInputWindowLayoutParameters() + } + + /** Updates the layout params of the window and input view. */ + private fun updateSoftInputWindowLayoutParameters() { + val w = window.window ?: return + if (inputView != null) { + val layoutHeight = + if (isFullscreenMode) { + WindowManager.LayoutParams.WRAP_CONTENT + } else { + WindowManager.LayoutParams.MATCH_PARENT + } + val inputArea = w.findViewById(android.R.id.inputArea) + // TODO: 需要获取到文本编辑框、完成按钮,设置其色彩和尺寸。 + // if (isFullscreenMode()) { + // Timber.d("isFullscreenMode"); + // /* In Fullscreen mode, when layout contains transparent color, + // * the background under input area will disturb users' typing, + // * so set the input area as light pink */ + // inputArea.setBackgroundColor(parseColor("#ff660000")); + // } else { + // Timber.d("NotFullscreenMode"); + // /* Otherwise, set it as light gray to avoid potential issue */ + // inputArea.setBackgroundColor(parseColor("#dddddddd")); + // } + updateLayoutHeightOf(inputArea, layoutHeight) + updateLayoutGravityOf(inputArea, Gravity.BOTTOM) + updateLayoutHeightOf(inputView!!, layoutHeight) + } + } + + fun addEventListener(listener: EventListener): Boolean { + return eventListeners.add(listener) + } + + fun removeEventListener(listener: EventListener): Boolean { + return eventListeners.remove(listener) + } + + interface EventListener { + fun onCreate() {} + + fun onInitializeInputUi(inputView: InputView) {} + + fun onDestroy() {} + + fun onStartInputView( + instance: EditorInstance, + restarting: Boolean, + ) {} + + fun osFinishInputView(finishingInput: Boolean) {} + + fun onWindowShown() {} + + fun onWindowHidden() {} + + fun onUpdateSelection() {} + } + + companion object { + var self: Trime? = null + + @JvmStatic + fun getService(): Trime { + return self ?: throw IllegalStateException("Trime not initialized") + } + + fun getServiceOrNull(): Trime? { + return self + } + + private val syncBackgroundHandler = + Handler( + Looper.getMainLooper(), + ) { msg: Message -> + // 若当前没有输入面板,则后台同步。防止面板关闭后5秒内再次打开 + if (!(msg.obj as Trime).isShowInputRequested) { + syncInBackground() + (msg.obj as Trime).loadConfig() + } + false + } + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt b/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt index 8392ffdc49..d8a7ea8ca7 100644 --- a/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt +++ b/app/src/main/java/com/osfans/trime/ime/text/TextInputManager.kt @@ -59,7 +59,7 @@ class TextInputManager private constructor(private val isDarkMode: Boolean) : private val trime get() = Trime.getService() private val prefs get() = AppPrefs.defaultInstance() private val activeEditorInstance: EditorInstance - get() = trime.activeEditorInstance + get() = trime.activeEditorInstance as EditorInstance private var intentReceiver: IntentReceiver? = null private var rimeNotiHandlerJob: Job? = null @@ -243,7 +243,7 @@ class TextInputManager private constructor(private val isDarkMode: Boolean) : val value = notification.value when (val option = notification.option) { "ascii_mode" -> { - trime.inputFeedbackManager.ttsLanguage = + trime.inputFeedbackManager?.ttsLanguage = locales[if (value) 1 else 0] } "_hide_comment" -> trime.setShowComment(!value)