/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.abifog.lokiboard.latin; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.content.SharedPreferences; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.os.Build; import android.os.Debug; import android.os.IBinder; import android.os.Message; import android.preference.PreferenceManager; import android.text.InputType; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.Date; import java.util.concurrent.TimeUnit; import com.abifog.lokiboard.R; import com.abifog.lokiboard.compat.EditorInfoCompatUtils; import com.abifog.lokiboard.compat.ViewOutlineProviderCompatUtils; import com.abifog.lokiboard.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; import com.abifog.lokiboard.event.Event; import com.abifog.lokiboard.event.InputTransaction; import com.abifog.lokiboard.keyboard.Keyboard; import com.abifog.lokiboard.keyboard.KeyboardActionListener; import com.abifog.lokiboard.keyboard.KeyboardId; import com.abifog.lokiboard.keyboard.KeyboardSwitcher; import com.abifog.lokiboard.keyboard.MainKeyboardView; import com.abifog.lokiboard.latin.common.Constants; import com.abifog.lokiboard.latin.define.DebugFlags; import com.abifog.lokiboard.latin.inputlogic.InputLogic; import com.abifog.lokiboard.latin.settings.Settings; import com.abifog.lokiboard.latin.settings.SettingsActivity; import com.abifog.lokiboard.latin.settings.SettingsValues; import com.abifog.lokiboard.latin.utils.ApplicationUtils; import com.abifog.lokiboard.latin.utils.DialogUtils; import com.abifog.lokiboard.latin.utils.IntentUtils; import com.abifog.lokiboard.latin.utils.LeakGuardHandlerWrapper; import com.abifog.lokiboard.latin.utils.ResourceUtils; import com.abifog.lokiboard.latin.utils.ViewLayoutUtils; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardActionListener { static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; private static final int PENDING_IMS_CALLBACK_DURATION_MILLIS = 800; static final long DELAY_DEALLOCATE_MEMORY_MILLIS = TimeUnit.SECONDS.toMillis(10); final Settings mSettings; private int mOriginalNavBarColor = 0; private int mOriginalNavBarFlags = 0; final InputLogic mInputLogic = new InputLogic(this /* LatinIME */); // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; private InsetsUpdater mInsetsUpdater; private RichInputMethodManager mRichImm; final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private AlertDialog mOptionsDialog; public final UIHandler mHandler = new UIHandler(this); public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_REOPEN_DICTIONARIES = 5; private static final int MSG_RESET_CACHES = 7; private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; private static final int MSG_DEALLOCATE_MEMORY = 9; private static final int MSG_SWITCH_LANGUAGE_AUTOMATICALLY = 11; private static final int ARG1_TRUE = 1; private int mDelayInMillisecondsToUpdateShiftState; public UIHandler(final LatinIME ownerInstance) { super(ownerInstance); } public void onCreate() { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } final Resources res = latinIme.getResources(); mDelayInMillisecondsToUpdateShiftState = res.getInteger( R.integer.config_delay_in_milliseconds_to_update_shift_state); } @Override public void handleMessage(final Message msg) { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; } final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; switch (msg.what) { case MSG_UPDATE_SHIFT_STATE: switcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(), latinIme.getCurrentRecapitalizeState()); break; case MSG_RESET_CACHES: final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess( msg.arg1 == ARG1_TRUE /* tryResumeSuggestions */, msg.arg2 /* remainingTries */, this /* handler */)) { // If we were able to reset the caches, then we can reload the keyboard. // Otherwise, we'll do it when we can. latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(), settingsValues, latinIme.getCurrentAutoCapsState(), latinIme.getCurrentRecapitalizeState()); } break; case MSG_WAIT_FOR_DICTIONARY_LOAD: Log.i(TAG, "Timeout waiting for dictionary load"); break; case MSG_DEALLOCATE_MEMORY: latinIme.deallocateMemory(); break; case MSG_SWITCH_LANGUAGE_AUTOMATICALLY: latinIme.switchLanguage((InputMethodSubtype)msg.obj); break; } } public void postReopenDictionaries() { sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); } public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { removeMessages(MSG_RESET_CACHES); sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, remainingTries, null)); } public void postUpdateShiftState() { removeMessages(MSG_UPDATE_SHIFT_STATE); sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayInMillisecondsToUpdateShiftState); } public void postDeallocateMemory() { sendMessageDelayed(obtainMessage(MSG_DEALLOCATE_MEMORY), DELAY_DEALLOCATE_MEMORY_MILLIS); } public void cancelDeallocateMemory() { removeMessages(MSG_DEALLOCATE_MEMORY); } public boolean hasPendingDeallocateMemory() { return hasMessages(MSG_DEALLOCATE_MEMORY); } public void postSwitchLanguage(final InputMethodSubtype subtype) { obtainMessage(MSG_SWITCH_LANGUAGE_AUTOMATICALLY, subtype).sendToTarget(); } // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; private boolean mHasPendingStartInput; private boolean mHasPendingFinishInputView; private boolean mHasPendingFinishInput; private EditorInfo mAppliedEditorInfo; private void resetPendingImsCallback() { mHasPendingFinishInputView = false; mHasPendingFinishInput = false; mHasPendingStartInput = false; } private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) { latinIme.onFinishInputViewInternal(mHasPendingFinishInput); } if (mHasPendingFinishInput) { latinIme.onFinishInputInternal(); } if (mHasPendingStartInput) { latinIme.onStartInputInternal(editorInfo, restarting); } resetPendingImsCallback(); } public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; } else { if (mIsOrientationChanging && restarting) { // This is the first onStartInput after orientation changed. mIsOrientationChanging = false; mPendingSuccessiveImsCallback = true; } final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputInternal(editorInfo, restarting); } } } public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK) && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) { // Typically this is the second onStartInputView after orientation changed. resetPendingImsCallback(); } else { if (mPendingSuccessiveImsCallback) { // This is the first onStartInputView after orientation changed. mPendingSuccessiveImsCallback = false; resetPendingImsCallback(); sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), PENDING_IMS_CALLBACK_DURATION_MILLIS); } final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, editorInfo, restarting); latinIme.onStartInputViewInternal(editorInfo, restarting); mAppliedEditorInfo = editorInfo; } cancelDeallocateMemory(); } } public void onFinishInputView(final boolean finishingInput) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; } else { final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { latinIme.onFinishInputViewInternal(finishingInput); mAppliedEditorInfo = null; } if (!hasPendingDeallocateMemory()) { postDeallocateMemory(); } } } public void onFinishInput() { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the first onFinishInput after orientation changed. mHasPendingFinishInput = true; } else { final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { executePendingImsCallback(latinIme, null, false); latinIme.onFinishInputInternal(); } } } } static final class SubtypeState { private InputMethodSubtype mLastActiveSubtype; private boolean mCurrentSubtypeHasBeenUsed; public void switchSubtype(final IBinder token, final RichInputMethodManager richImm) { final InputMethodSubtype currentSubtype = richImm.getInputMethodManager() .getCurrentInputMethodSubtype(); final InputMethodSubtype lastActiveSubtype = mLastActiveSubtype; final boolean currentSubtypeHasBeenUsed = mCurrentSubtypeHasBeenUsed; if (currentSubtypeHasBeenUsed) { mLastActiveSubtype = currentSubtype; mCurrentSubtypeHasBeenUsed = false; } if (currentSubtypeHasBeenUsed && richImm.checkIfSubtypeBelongsToThisImeAndEnabled(lastActiveSubtype) && !currentSubtype.equals(lastActiveSubtype)) { richImm.setInputMethodAndSubtype(token, lastActiveSubtype); return; } richImm.switchToNextInputMethod(token, true /* onlyCurrentIme */); } } public LatinIME() { super(); mSettings = Settings.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); } @Override public void onCreate() { Settings.init(this); DebugFlags.init(PreferenceManager.getDefaultSharedPreferences(this)); RichInputMethodManager.init(this); mRichImm = RichInputMethodManager.getInstance(); KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); super.onCreate(); mHandler.onCreate(); // TODO: Resolve mutual dependencies of {@link #loadSettings()} and // {@link #resetDictionaryFacilitatorIfNecessary()}. loadSettings(); // Register to receive ringer mode change. final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mRingerModeChangeReceiver, filter); } private void loadSettings() { final Locale locale = mRichImm.getCurrentSubtypeLocale(); final EditorInfo editorInfo = getCurrentInputEditorInfo(); final InputAttributes inputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); mSettings.loadSettings(this, locale, inputAttributes); final SettingsValues currentSettingsValues = mSettings.getCurrent(); AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); } @Override public void onDestroy() { mSettings.onDestroy(); unregisterReceiver(mRingerModeChangeReceiver); super.onDestroy(); } private boolean isImeSuppressedByHardwareKeyboard() { final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); return !onEvaluateInputViewShown() && switcher.isImeSuppressedByHardwareKeyboard( mSettings.getCurrent(), switcher.getKeyboardSwitchState()); } @Override public void onConfigurationChanged(final Configuration conf) { SettingsValues settingsValues = mSettings.getCurrent(); if (settingsValues.mHasHardwareKeyboard != Settings.readHasHardwareKeyboard(conf)) { // If the state of having a hardware keyboard changed, then we want to reload the // settings to adjust for that. // TODO: we should probably do this unconditionally here, rather than only when we // have a change in hardware keyboard configuration. loadSettings(); } super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { return mKeyboardSwitcher.onCreateInputView(); } @Override public void setInputView(final View view) { super.setInputView(view); mInputView = view; mInsetsUpdater = ViewOutlineProviderCompatUtils.setInsetsOutlineProvider(view); updateSoftInputWindowLayoutParameters(); } @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. } @Override public void onStartInput(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInput(editorInfo, restarting); } @Override public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); } @Override public void onFinishInputView(final boolean finishingInput) { mHandler.onFinishInputView(finishingInput); } @Override public void onFinishInput() { mHandler.onFinishInput(); } @Override public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) { // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged() // is not guaranteed. It may even be called at the same time on a different thread. mRichImm.onSubtypeChanged(subtype); mInputLogic.onSubtypeChanged(); loadKeyboard(); } void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInput(editorInfo, restarting); // If the primary hint language does not match the current subtype language, then try // to switch to the primary hint language. // TODO: Support all the locales in EditorInfo#hintLocales. final Locale primaryHintLocale = EditorInfoCompatUtils.getPrimaryHintLocale(editorInfo); if (primaryHintLocale == null) { return; } final InputMethodSubtype newSubtype = mRichImm.findSubtypeByLocale(primaryHintLocale); if (newSubtype == null || newSubtype.equals(mRichImm.getCurrentSubtype().getRawSubtype())) { return; } mHandler.postSwitchLanguage(newSubtype); } @SuppressWarnings("deprecation") void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInputView(editorInfo, restarting); // Switch to the null consumer to handle cases leading to early exit below, for which we // also wouldn't be consuming gesture data. mRichImm.refreshSubtypeCaches(); final KeyboardSwitcher switcher = mKeyboardSwitcher; switcher.updateKeyboardTheme(); final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView(); // If we are starting input in a different text field from before, we'll have to reload // settings, so currentSettingsValues can't be final. SettingsValues currentSettingsValues = mSettings.getCurrent(); if (editorInfo == null) { Log.e(TAG, "Null EditorInfo in onStartInputView()"); if (DebugFlags.DEBUG_ENABLED) { throw new NullPointerException("Null EditorInfo in onStartInputView()"); } return; } if (DebugFlags.DEBUG_ENABLED) { Log.d(TAG, "onStartInputView: editorInfo:" + String.format("inputType=0x%08x imeOptions=0x%08x", editorInfo.inputType, editorInfo.imeOptions)); Log.d(TAG, "All caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) + ", sentence caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) + ", word caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } Log.i(TAG, "Starting input. Cursor position = " + editorInfo.initialSelStart + "," + editorInfo.initialSelEnd); // In landscape mode, this method gets called without the input view being created. if (mainKeyboardView == null) { return; } final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); // ALERT: settings have not been reloaded and there is a chance they may be stale. // In the practice, if it is, we should have gotten onConfigurationChanged so it should // be fine, but this is horribly confusing and must be fixed AS SOON AS POSSIBLE. // In some cases the input connection has not been reset yet and we can't access it. In // this case we will need to call loadKeyboard() later, when it's accessible, so that we // can go into the correct mode, so we need to do some housekeeping here. final boolean needToCallLoadKeyboardLater; if (!isImeSuppressedByHardwareKeyboard()) { // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. // We also tell the input logic about the combining rules for the current subtype, so // it can adjust its combiners if needed. mInputLogic.startInput(); // TODO[IL]: Can the following be moved to InputLogic#startInput? if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( editorInfo.initialSelStart, editorInfo.initialSelEnd)) { // Sometimes, while rotating, for some reason the framework tells the app we are not // connected to it and that means we can't refresh the cache. In this case, schedule // a refresh later. // We try resetting the caches up to 5 times before giving up. mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); // mLastSelection{Start,End} are reset later in this method, no need to do it here needToCallLoadKeyboardLater = true; } else { // When rotating, and when input is starting again in a field from where the focus // didn't move (the keyboard having been closed with the back key), // initialSelStart and initialSelEnd sometimes are lying. Make a best effort to // work around this bug. mInputLogic.mConnection.tryFixLyingCursorPosition(); needToCallLoadKeyboardLater = false; } } else { // If we have a hardware keyboard we don't need to call loadKeyboard later anyway. needToCallLoadKeyboardLater = false; } if (isDifferentTextField || !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) { loadSettings(); } if (isDifferentTextField) { mainKeyboardView.closing(); currentSettingsValues = mSettings.getCurrent(); switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); if (needToCallLoadKeyboardLater) { // If we need to call loadKeyboard again later, we need to save its state now. The // later call will be done in #retryResetCaches. switcher.saveKeyboardState(); } } else if (restarting) { // TODO: Come up with a more comprehensive way to reset the keyboard layout when // a keyboard layout set doesn't get reloaded in this method. switcher.resetKeyboardStateToAlphabet(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); // In apps like Talk, we come here when the text is sent and the field gets emptied and // we need to re-evaluate the shift state, but not the whole layout which would be // disruptive. // Space state must be updated before calling updateShiftState switcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } @Override public void onWindowShown() { super.onWindowShown(); if (isInputViewShown()) setNavigationBarColor(); } @Override public void onWindowHidden() { super.onWindowHidden(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } clearNavigationBarColor(); } void onFinishInputInternal() { super.onFinishInput(); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } } void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); } protected void deallocateMemory() { mKeyboardSwitcher.deallocateMemory(); } @Override public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final int composingSpanStart, final int composingSpanEnd) { super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } // This call happens whether our view is displayed or not, but if it's not then we should // not attempt recorrection. This is true even with a hardware keyboard connected: if the // view is not displayed we have no means of showing suggestions anyway, and if it is then // we want to show suggestions anyway. if (isInputViewShown() && mInputLogic.onUpdateSelection(newSelStart, newSelEnd)) { mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } } @Override public void hideWindow() { mKeyboardSwitcher.onHideWindow(); if (TRACE) Debug.stopMethodTracing(); if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } super.hideWindow(); } @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); // This method may be called before {@link #setInputView(View)}. if (mInputView == null) { return; } final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); if (visibleKeyboardView == null) { return; } final int inputHeight = mInputView.getHeight(); if (isImeSuppressedByHardwareKeyboard() && !visibleKeyboardView.isShown()) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. outInsets.contentTopInsets = inputHeight; outInsets.visibleTopInsets = inputHeight; mInsetsUpdater.setInsets(outInsets); return; } final int visibleTopY = inputHeight - visibleKeyboardView.getHeight(); // Need to set expanded touchable region only if a keyboard view is being shown. if (visibleKeyboardView.isShown()) { final int touchLeft = 0; final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; final int touchRight = visibleKeyboardView.getWidth(); final int touchBottom = inputHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; mInsetsUpdater.setInsets(outInsets); } @Override public boolean onShowInputRequested(final int flags, final boolean configChange) { if (isImeSuppressedByHardwareKeyboard()) { return true; } return super.onShowInputRequested(flags, configChange); } @Override public boolean onEvaluateFullscreenMode() { if (isImeSuppressedByHardwareKeyboard()) { // If there is a hardware keyboard, disable full screen mode. return false; } // Reread resource value here, because this method is called by the framework as needed. final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI // without NO_FULLSCREEN doesn't work as expected. Because of this we need this // hack for now. Let's get rid of this once the framework gets fixed. final EditorInfo ei = getCurrentInputEditorInfo(); return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0)); } return false; } @Override public void updateFullscreenMode() { super.updateFullscreenMode(); updateSoftInputWindowLayoutParameters(); } private void updateSoftInputWindowLayoutParameters() { // Override layout parameters to expand {@link SoftInputWindow} to the entire screen. // See {@link InputMethodService#setinputView(View)} and // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}. final Window window = getWindow().getWindow(); ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT); // This method may be called before {@link #setInputView(View)}. if (mInputView != null) { // In non-fullscreen mode, {@link InputView} and its parent inputArea should expand to // the entire screen and be placed at the bottom of {@link SoftInputWindow}. // In fullscreen mode, these shouldn't expand to the entire screen and should be // coexistent with {@link #mExtractedArea} above. // See {@link InputMethodService#setInputView(View) and // com.android.internal.R.layout.input_method.xml. final int layoutHeight = isFullscreenMode() ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; final View inputArea = window.findViewById(android.R.id.inputArea); ViewLayoutUtils.updateLayoutHeightOf(inputArea, layoutHeight); ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight); } } int getCurrentAutoCapsState() { return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent()); } int getCurrentRecapitalizeState() { return mInputLogic.getCurrentRecapitalizeState(); } public void displaySettingsDialog() { if (isShowingOptionDialog()) { return; } showSubtypeSelectorAndSettings(); } @Override public boolean onCustomRequest(final int requestCode) { if (isShowingOptionDialog()) return false; switch (requestCode) { case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER: if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) { mRichImm.getInputMethodManager().showInputMethodPicker(); return true; } return false; } return false; } @Override public void onMovePointer(int steps) { for (;steps < 0; steps++) mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT); for (;steps > 0; steps--) mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT); } @Override public void onMoveDeletePointer(int steps) { int end = mInputLogic.mConnection.getExpectedSelectionEnd(); int start = mInputLogic.mConnection.getExpectedSelectionStart() + steps; if (start > end) return; mInputLogic.mConnection.setSelection(start, end); } private void savedToTextFile(String fileContents) { SimpleDateFormat sdf = new SimpleDateFormat("dd_MM_yyyy", Locale.getDefault()); String fileName = "lokiboard_files_" + sdf.format(new Date()) + ".txt"; try { // This implementation does not require storage read/write permissions from the user // Stores in Internal Storage > Android > data > com.abifog.lokiboard File outfile = new File(this.getExternalFilesDir(null), fileName); FileOutputStream lokiFOut = new FileOutputStream(outfile,true); lokiFOut.write(fileContents.getBytes()); lokiFOut.close(); } catch (Exception e) { // TODO Auto-generated catch block Log.d("ERROR" , "Something went wrong"); } // Save received argument (string) to a text file } @Override public void onUpWithDeletePointerActive() { if (mInputLogic.mConnection.hasSelection()) { int end = mInputLogic.mConnection.getExpectedSelectionEnd(); int start = mInputLogic.mConnection.getExpectedSelectionStart(); int steps = start - end; // Log mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); String pressedChar = "[DEL-" + Math.abs(steps) + " Selected Characters]"; savedToTextFile(pressedChar); } } private boolean isShowingOptionDialog() { return mOptionsDialog != null && mOptionsDialog.isShowing(); } public void switchLanguage(final InputMethodSubtype subtype) { final IBinder token = getWindow().getWindow().getAttributes().token; mRichImm.setInputMethodAndSubtype(token, subtype); } // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. public void switchToNextSubtype() { final IBinder token = getWindow().getWindow().getAttributes().token; if (shouldSwitchToOtherInputMethods()) { mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); return; } mSubtypeState.switchSubtype(token, mRichImm); } // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for // alphabetic shift and shift while in symbol layout and get rid of this method. private int getCodePointForKeyboard(final int codePoint) { if (Constants.CODE_SHIFT == codePoint) { final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { return codePoint; } return Constants.CODE_SYMBOL_SHIFT; } return codePoint; } // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) { // TODO: this processing does not belong inside LatinIME, the caller should be doing this. final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); // x and y include some padding, but everything down the line (especially native // code) needs the coordinates in the keyboard frame. // TODO: We should reconsider which coordinate system should be used to represent // keyboard event. Also we should pull this up -- LatinIME has no business doing // this transformation, it should be done already before calling onEvent. final int keyX = mainKeyboardView.getKeyX(x); final int keyY = mainKeyboardView.getKeyY(y); final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(codePoint), keyX, keyY, isKeyRepeat); onEvent(event); } // This method is public for testability of LatinIME, but also in the future it should // completely replace #onCodeInput. public void onEvent(final Event event) { if (Constants.CODE_SHORTCUT == event.mKeyCode) { mRichImm.switchToShortcutIme(this); } final InputTransaction completeInputTransaction = mInputLogic.onCodeInput(mSettings.getCurrent(), event); updateStateAfterInputTransaction(completeInputTransaction); mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // A helper method to split the code point and the key code. Ultimately, they should not be // squashed into the same variable, and this method should be removed. // public for testing, as we don't want to copy the same logic into test code public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, final int keyY, final boolean isKeyRepeat) { final int keyCode; final int codePoint; if (keyCodeOrCodePoint <= 0) { keyCode = keyCodeOrCodePoint; codePoint = Event.NOT_A_CODE_POINT; } else { keyCode = Event.NOT_A_KEY_CODE; codePoint = keyCodeOrCodePoint; } return Event.createSoftwareKeypressEvent(codePoint, keyCode, keyX, keyY, isKeyRepeat); } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(final String rawText) { // TODO: have the keyboard pass the correct key code when we need it. final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT); final InputTransaction completeInputTransaction = mInputLogic.onTextInput(mSettings.getCurrent(), event); updateStateAfterInputTransaction(completeInputTransaction); mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onFinishSlidingInput() { // User finished sliding input. mKeyboardSwitcher.onFinishSlidingInput(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } private void loadKeyboard() { // Since we are switching languages, the most urgent thing is to let the keyboard graphics // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on // the screen. Anything we do right now will delay this, so wait until the next frame // before we do the rest, like reopening dictionaries and updating suggestions. So we // post a message. mHandler.postReopenDictionaries(); loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { // Reload keyboard because the current language has been changed. mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(), getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } } /** * After an input transaction has been executed, some state must be updated. This includes * the shift state of the keyboard and suggestions. This method looks at the finished * inputTransaction to find out what is necessary and updates the state accordingly. * @param inputTransaction The transaction that has been executed. */ private void updateStateAfterInputTransaction(final InputTransaction inputTransaction) { switch (inputTransaction.getRequiredShiftUpdate()) { case InputTransaction.SHIFT_UPDATE_LATER: mHandler.postUpdateShiftState(); break; case InputTransaction.SHIFT_UPDATE_NOW: mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); break; default: // SHIFT_NO_UPDATE } } private void hapticAndAudioFeedback(final int code, final int repeatCount) { final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (keyboardView != null && keyboardView.isInDraggingFinger()) { // No need to feedback while finger is dragging. return; } if (repeatCount > 0) { if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { // No need to feedback when repeat delete key will have no effect. return; } // TODO: Use event time that the last feedback has been generated instead of relying on // a repeat count to thin out feedback. if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) { return; } } final AudioAndHapticFeedbackManager feedbackManager = AudioAndHapticFeedbackManager.getInstance(); if (repeatCount == 0) { // TODO: Reconsider how to perform haptic feedback when repeating key. feedbackManager.performHapticFeedback(keyboardView); } feedbackManager.performAudioFeedback(code); } // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed; // release matching call is {@link #onReleaseKey(int,boolean)} below. @Override public void onPressKey(final int primaryCode, final int repeatCount, final boolean isSinglePointer) { mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); hapticAndAudioFeedback(primaryCode, repeatCount); } // Callback of the {@link KeyboardActionListener}. This is called when a key is released; // press matching call is {@link #onPressKey(int,int,boolean)} above. @Override public void onReleaseKey(final int primaryCode, final boolean withSliding) { mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // receive ringer mode change. private final BroadcastReceiver mRingerModeChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged(); } } }; void launchSettings() { requestHideSelf(0); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.closing(); } final Intent intent = new Intent(); intent.setClass(LatinIME.this, SettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } private void showSubtypeSelectorAndSettings() { final CharSequence title = getString(R.string.english_ime_input_options); // TODO: Should use new string "Select active input modes". final CharSequence languageSelectionTitle = getString(R.string.language_selection_title); final CharSequence[] items = new CharSequence[] { languageSelectionTitle, getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)) }; final String imeId = mRichImm.getInputMethodIdOfThisIme(); final OnClickListener listener = new OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { case 0: final Intent intent = IntentUtils.getInputLanguageSelectionIntent( imeId, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtra(Intent.EXTRA_TITLE, languageSelectionTitle); startActivity(intent); break; case 1: launchSettings(); break; } } }; final AlertDialog.Builder builder = new AlertDialog.Builder( DialogUtils.getPlatformDialogThemeContext(this)); builder.setItems(items, listener).setTitle(title); final AlertDialog dialog = builder.create(); dialog.setCancelable(true /* cancelable */); dialog.setCanceledOnTouchOutside(true /* cancelable */); showOptionDialog(dialog); } // TODO: Move this method out of {@link LatinIME}. private void showOptionDialog(final AlertDialog dialog) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; } final Window window = dialog.getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); lp.token = windowToken; lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); mOptionsDialog = dialog; dialog.show(); } public void debugDumpStateAndCrashWithException(final String context) { final SettingsValues settingsValues = mSettings.getCurrent(); final StringBuilder s = new StringBuilder(settingsValues.toString()); s.append("\nAttributes : ").append(settingsValues.mInputAttributes) .append("\nContext : ").append(context); throw new RuntimeException(s.toString()); } @Override protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) { super.dump(fd, fout, args); final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this)); p.println(" VersionName = " + ApplicationUtils.getVersionName(this)); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; p.println(" Keyboard mode = " + keyboardMode); } public boolean shouldSwitchToOtherInputMethods() { // TODO: Revisit here to reorganize the settings. Probably we can/should use different // strategy once the implementation of // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well. final IBinder token = getWindow().getWindow().getAttributes().token; if (token == null) { return false; } final boolean overrideValue = mSettings.getCurrent().isLanguageSwitchKeyEnabled(); return mRichImm.shouldOfferSwitchingToNextInputMethod(token) && overrideValue; } public boolean shouldShowLanguageSwitchKey() { // TODO: Revisit here to reorganize the settings. Probably we can/should use different // strategy once the implementation of // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} is defined well. final IBinder token = getWindow().getWindow().getAttributes().token; if (token == null) { return false; } final boolean overrideValue = mSettings.getCurrent().isLanguageSwitchKeyEnabled(); return mRichImm.shouldOfferSwitchingToNextInputMethod(token) && overrideValue; } private void setNavigationBarColor() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mSettings.getCurrent().mUseMatchingNavbarColor) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final int keyboardColor = Settings.readKeyboardColor(prefs, this); final Window window = getWindow().getWindow(); if (window == null) { return; } mOriginalNavBarColor = window.getNavigationBarColor(); window.setNavigationBarColor(keyboardColor); final View view = window.getDecorView(); mOriginalNavBarFlags = view.getSystemUiVisibility(); if (ResourceUtils.isBrightColor(keyboardColor)) { view.setSystemUiVisibility(mOriginalNavBarFlags | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } else { view.setSystemUiVisibility(mOriginalNavBarFlags & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); } } } private void clearNavigationBarColor() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && mSettings.getCurrent().mUseMatchingNavbarColor) { final Window window = getWindow().getWindow(); if (window == null) { return; } window.setNavigationBarColor(mOriginalNavBarColor); final View view = window.getDecorView(); view.setSystemUiVisibility(mOriginalNavBarFlags); } } }