/* * Copyright (C) 2007 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.android.internal.inputmethod; import static android.view.inputmethod.InputConnectionProto.CURSOR_CAPS_MODE; import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_END; import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_START; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.RectF; import android.os.Bundle; import android.os.CancellationSignal; import android.text.Editable; import android.text.Selection; import android.text.method.KeyListener; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.DeleteGesture; import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.DumpableInputConnection; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InsertGesture; import android.view.inputmethod.InsertModeGesture; import android.view.inputmethod.JoinOrSplitGesture; import android.view.inputmethod.PreviewableHandwritingGesture; import android.view.inputmethod.RemoveSpaceGesture; import android.view.inputmethod.SelectGesture; import android.view.inputmethod.SelectRangeGesture; import android.view.inputmethod.TextBoundsInfo; import android.view.inputmethod.TextBoundsInfoResult; import android.widget.TextView; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; /** * Base class for an editable InputConnection instance. This is created by {@link TextView} or * {@link android.widget.EditText}. */ public final class EditableInputConnection extends BaseInputConnection implements DumpableInputConnection { private static final boolean DEBUG = false; private static final String TAG = "EditableInputConnection"; private final TextView mTextView; // Keeps track of nested begin/end batch edit to ensure this connection always has a // balanced impact on its associated TextView. // A negative value means that this connection has been finished by the InputMethodManager. private int mBatchEditNesting; public EditableInputConnection(TextView textview) { super(textview, true); mTextView = textview; } @Override public Editable getEditable() { TextView tv = mTextView; if (tv != null) { return tv.getEditableText(); } return null; } @Override public boolean beginBatchEdit() { synchronized (this) { if (mBatchEditNesting >= 0) { mTextView.beginBatchEdit(); mBatchEditNesting++; return true; } } return false; } @Override public boolean endBatchEdit() { synchronized (this) { if (mBatchEditNesting > 0) { // When the connection is reset by the InputMethodManager and reportFinish // is called, some endBatchEdit calls may still be asynchronously received from the // IME. Do not take these into account, thus ensuring that this IC's final // contribution to mTextView's nested batch edit count is zero. mTextView.endBatchEdit(); mBatchEditNesting--; return mBatchEditNesting > 0; } } return false; } @Override public void endComposingRegionEditInternal() { // The ContentCapture service is interested in Composing-state changes. mTextView.notifyContentCaptureTextChanged(); } @Override public void closeConnection() { super.closeConnection(); synchronized (this) { while (mBatchEditNesting > 0) { endBatchEdit(); } // Will prevent any further calls to begin or endBatchEdit mBatchEditNesting = -1; } } @Override public boolean clearMetaKeyStates(int states) { final Editable content = getEditable(); if (content == null) return false; KeyListener kl = mTextView.getKeyListener(); if (kl != null) { try { kl.clearMetaKeyState(mTextView, content, states); } catch (AbstractMethodError e) { // This is an old listener that doesn't implement the // new method. } } return true; } @Override public boolean commitCompletion(CompletionInfo text) { if (DEBUG) Log.v(TAG, "commitCompletion " + text); mTextView.beginBatchEdit(); mTextView.onCommitCompletion(text); mTextView.endBatchEdit(); return true; } /** * Calls the {@link TextView#onCommitCorrection} method of the associated TextView. */ @Override public boolean commitCorrection(CorrectionInfo correctionInfo) { if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo); mTextView.beginBatchEdit(); mTextView.onCommitCorrection(correctionInfo); mTextView.endBatchEdit(); return true; } @Override public boolean performEditorAction(int actionCode) { if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode); mTextView.onEditorAction(actionCode); return true; } @Override public boolean performContextMenuAction(int id) { if (DEBUG) Log.v(TAG, "performContextMenuAction " + id); mTextView.beginBatchEdit(); mTextView.onTextContextMenuItem(id); mTextView.endBatchEdit(); return true; } @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { if (mTextView != null) { ExtractedText et = new ExtractedText(); if (mTextView.extractText(request, et)) { if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) { mTextView.setExtracting(request); } return et; } } return null; } @Override public boolean performSpellCheck() { mTextView.onPerformSpellCheck(); return true; } @Override public boolean performPrivateCommand(String action, Bundle data) { mTextView.onPrivateIMECommand(action, data); return true; } @Override public boolean commitText(CharSequence text, int newCursorPosition) { if (mTextView == null) { return super.commitText(text, newCursorPosition); } mTextView.resetErrorChangedFlag(); boolean success = super.commitText(text, newCursorPosition); mTextView.hideErrorIfUnchanged(); return success; } @Override public boolean requestCursorUpdates( @CursorUpdateMode int cursorUpdateMode, @CursorUpdateFilter int cursorUpdateFilter) { // TODO(b/210039666): use separate attrs for updateMode and updateFilter. return requestCursorUpdates(cursorUpdateMode | cursorUpdateFilter); } @Override public boolean requestCursorUpdates(int cursorUpdateMode) { if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode); final int knownModeFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR; final int knownFilterFlags = InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS | InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS | InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS | InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE; // It is possible that any other bit is used as a valid flag in a future release. // We should reject the entire request in such a case. final int knownFlagMask = knownModeFlags | knownFilterFlags; final int unknownFlags = cursorUpdateMode & ~knownFlagMask; if (unknownFlags != 0) { if (DEBUG) { Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags. " + "cursorUpdateMode=" + cursorUpdateMode + " unknownFlags=" + unknownFlags); } return false; } if (mIMM == null) { // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled. // TODO: Return some notification code rather than false to indicate method that // CursorAnchorInfo is temporarily unavailable. return false; } mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode); // for UnsupportedAppUsage if (mTextView != null) { mTextView.onRequestCursorUpdatesInternal(cursorUpdateMode & knownModeFlags, cursorUpdateMode & knownFilterFlags); } return true; } @Override public void requestTextBoundsInfo( @NonNull RectF bounds, @Nullable @CallbackExecutor Executor executor, @NonNull Consumer consumer) { final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(bounds); final int resultCode; if (textBoundsInfo != null) { resultCode = TextBoundsInfoResult.CODE_SUCCESS; } else { resultCode = TextBoundsInfoResult.CODE_FAILED; } final TextBoundsInfoResult textBoundsInfoResult = new TextBoundsInfoResult(resultCode, textBoundsInfo); executor.execute(() -> consumer.accept(textBoundsInfoResult)); } @Override public boolean setImeConsumesInput(boolean imeConsumesInput) { if (mTextView == null) { return super.setImeConsumesInput(imeConsumesInput); } mTextView.setImeConsumesInput(imeConsumesInput); return true; } @Override public void performHandwritingGesture( @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer) { int result; if (gesture instanceof SelectGesture) { result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture); } else if (gesture instanceof SelectRangeGesture) { result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture); } else if (gesture instanceof DeleteGesture) { result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture); } else if (gesture instanceof DeleteRangeGesture) { result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture); } else if (gesture instanceof InsertGesture) { result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture); } else if (gesture instanceof RemoveSpaceGesture) { result = mTextView.performHandwritingRemoveSpaceGesture((RemoveSpaceGesture) gesture); } else if (gesture instanceof JoinOrSplitGesture) { result = mTextView.performHandwritingJoinOrSplitGesture((JoinOrSplitGesture) gesture); } else if (gesture instanceof InsertModeGesture) { result = mTextView.performHandwritingInsertModeGesture((InsertModeGesture) gesture); } else { result = HANDWRITING_GESTURE_RESULT_UNSUPPORTED; } if (executor != null && consumer != null) { executor.execute(() -> consumer.accept(result)); } } @Override public boolean previewHandwritingGesture( @NonNull PreviewableHandwritingGesture gesture, @Nullable CancellationSignal cancellationSignal) { return mTextView.previewHandwritingGesture(gesture, cancellationSignal); } @Override public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); final Editable content = getEditable(); if (content != null) { int start = Selection.getSelectionStart(content); int end = Selection.getSelectionEnd(content); proto.write(SELECTED_TEXT_START, start); proto.write(SELECTED_TEXT_END, end); } proto.write(CURSOR_CAPS_MODE, getCursorCapsMode(0)); proto.end(token); } }