1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.view;
18
19import android.annotation.NonNull;
20import android.inputmethodservice.AbstractInputMethodService;
21import android.os.Bundle;
22import android.os.Handler;
23import android.os.RemoteException;
24import android.os.SystemClock;
25import android.util.Log;
26import android.view.KeyEvent;
27import android.view.inputmethod.CompletionInfo;
28import android.view.inputmethod.CorrectionInfo;
29import android.view.inputmethod.ExtractedText;
30import android.view.inputmethod.ExtractedTextRequest;
31import android.view.inputmethod.InputConnection;
32import android.view.inputmethod.InputConnectionInspector;
33import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
34import android.view.inputmethod.InputContentInfo;
35
36import java.lang.ref.WeakReference;
37
38public class InputConnectionWrapper implements InputConnection {
39    private static final int MAX_WAIT_TIME_MILLIS = 2000;
40    private final IInputContext mIInputContext;
41    @NonNull
42    private final WeakReference<AbstractInputMethodService> mInputMethodService;
43
44    @MissingMethodFlags
45    private final int mMissingMethods;
46
47    static class InputContextCallback extends IInputContextCallback.Stub {
48        private static final String TAG = "InputConnectionWrapper.ICC";
49        public int mSeq;
50        public boolean mHaveValue;
51        public CharSequence mTextBeforeCursor;
52        public CharSequence mTextAfterCursor;
53        public CharSequence mSelectedText;
54        public ExtractedText mExtractedText;
55        public int mCursorCapsMode;
56        public boolean mRequestUpdateCursorAnchorInfoResult;
57        public boolean mCommitContentResult;
58
59        // A 'pool' of one InputContextCallback.  Each ICW request will attempt to gain
60        // exclusive access to this object.
61        private static InputContextCallback sInstance = new InputContextCallback();
62        private static int sSequenceNumber = 1;
63
64        /**
65         * Returns an InputContextCallback object that is guaranteed not to be in use by
66         * any other thread.  The returned object's 'have value' flag is cleared and its expected
67         * sequence number is set to a new integer.  We use a sequence number so that replies that
68         * occur after a timeout has expired are not interpreted as replies to a later request.
69         */
70        private static InputContextCallback getInstance() {
71            synchronized (InputContextCallback.class) {
72                // Return sInstance if it's non-null, otherwise construct a new callback
73                InputContextCallback callback;
74                if (sInstance != null) {
75                    callback = sInstance;
76                    sInstance = null;
77
78                    // Reset the callback
79                    callback.mHaveValue = false;
80                } else {
81                    callback = new InputContextCallback();
82                }
83
84                // Set the sequence number
85                callback.mSeq = sSequenceNumber++;
86                return callback;
87            }
88        }
89
90        /**
91         * Makes the given InputContextCallback available for use in the future.
92         */
93        private void dispose() {
94            synchronized (InputContextCallback.class) {
95                // If sInstance is non-null, just let this object be garbage-collected
96                if (sInstance == null) {
97                    // Allow any objects being held to be gc'ed
98                    mTextAfterCursor = null;
99                    mTextBeforeCursor = null;
100                    mExtractedText = null;
101                    sInstance = this;
102                }
103            }
104        }
105
106        public void setTextBeforeCursor(CharSequence textBeforeCursor, int seq) {
107            synchronized (this) {
108                if (seq == mSeq) {
109                    mTextBeforeCursor = textBeforeCursor;
110                    mHaveValue = true;
111                    notifyAll();
112                } else {
113                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
114                            + ") in setTextBeforeCursor, ignoring.");
115                }
116            }
117        }
118
119        public void setTextAfterCursor(CharSequence textAfterCursor, int seq) {
120            synchronized (this) {
121                if (seq == mSeq) {
122                    mTextAfterCursor = textAfterCursor;
123                    mHaveValue = true;
124                    notifyAll();
125                } else {
126                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
127                            + ") in setTextAfterCursor, ignoring.");
128                }
129            }
130        }
131
132        public void setSelectedText(CharSequence selectedText, int seq) {
133            synchronized (this) {
134                if (seq == mSeq) {
135                    mSelectedText = selectedText;
136                    mHaveValue = true;
137                    notifyAll();
138                } else {
139                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
140                            + ") in setSelectedText, ignoring.");
141                }
142            }
143        }
144
145        public void setCursorCapsMode(int capsMode, int seq) {
146            synchronized (this) {
147                if (seq == mSeq) {
148                    mCursorCapsMode = capsMode;
149                    mHaveValue = true;
150                    notifyAll();
151                } else {
152                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
153                            + ") in setCursorCapsMode, ignoring.");
154                }
155            }
156        }
157
158        public void setExtractedText(ExtractedText extractedText, int seq) {
159            synchronized (this) {
160                if (seq == mSeq) {
161                    mExtractedText = extractedText;
162                    mHaveValue = true;
163                    notifyAll();
164                } else {
165                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
166                            + ") in setExtractedText, ignoring.");
167                }
168            }
169        }
170
171        public void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq) {
172            synchronized (this) {
173                if (seq == mSeq) {
174                    mRequestUpdateCursorAnchorInfoResult = result;
175                    mHaveValue = true;
176                    notifyAll();
177                } else {
178                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
179                            + ") in setCursorAnchorInfoRequestResult, ignoring.");
180                }
181            }
182        }
183
184        public void setCommitContentResult(boolean result, int seq) {
185            synchronized (this) {
186                if (seq == mSeq) {
187                    mCommitContentResult = result;
188                    mHaveValue = true;
189                    notifyAll();
190                } else {
191                    Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq
192                            + ") in setCommitContentResult, ignoring.");
193                }
194            }
195        }
196
197        /**
198         * Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds.
199         *
200         * <p>The caller must be synchronized on this callback object.
201         */
202        void waitForResultLocked() {
203            long startTime = SystemClock.uptimeMillis();
204            long endTime = startTime + MAX_WAIT_TIME_MILLIS;
205
206            while (!mHaveValue) {
207                long remainingTime = endTime - SystemClock.uptimeMillis();
208                if (remainingTime <= 0) {
209                    Log.w(TAG, "Timed out waiting on IInputContextCallback");
210                    return;
211                }
212                try {
213                    wait(remainingTime);
214                } catch (InterruptedException e) {
215                }
216            }
217        }
218    }
219
220    public InputConnectionWrapper(
221            @NonNull WeakReference<AbstractInputMethodService> inputMethodService,
222            IInputContext inputContext, @MissingMethodFlags final int missingMethods) {
223        mInputMethodService = inputMethodService;
224        mIInputContext = inputContext;
225        mMissingMethods = missingMethods;
226    }
227
228    public CharSequence getTextAfterCursor(int length, int flags) {
229        CharSequence value = null;
230        try {
231            InputContextCallback callback = InputContextCallback.getInstance();
232            mIInputContext.getTextAfterCursor(length, flags, callback.mSeq, callback);
233            synchronized (callback) {
234                callback.waitForResultLocked();
235                if (callback.mHaveValue) {
236                    value = callback.mTextAfterCursor;
237                }
238            }
239            callback.dispose();
240        } catch (RemoteException e) {
241            return null;
242        }
243        return value;
244    }
245
246    public CharSequence getTextBeforeCursor(int length, int flags) {
247        CharSequence value = null;
248        try {
249            InputContextCallback callback = InputContextCallback.getInstance();
250            mIInputContext.getTextBeforeCursor(length, flags, callback.mSeq, callback);
251            synchronized (callback) {
252                callback.waitForResultLocked();
253                if (callback.mHaveValue) {
254                    value = callback.mTextBeforeCursor;
255                }
256            }
257            callback.dispose();
258        } catch (RemoteException e) {
259            return null;
260        }
261        return value;
262    }
263
264    public CharSequence getSelectedText(int flags) {
265        if (isMethodMissing(MissingMethodFlags.GET_SELECTED_TEXT)) {
266            // This method is not implemented.
267            return null;
268        }
269        CharSequence value = null;
270        try {
271            InputContextCallback callback = InputContextCallback.getInstance();
272            mIInputContext.getSelectedText(flags, callback.mSeq, callback);
273            synchronized (callback) {
274                callback.waitForResultLocked();
275                if (callback.mHaveValue) {
276                    value = callback.mSelectedText;
277                }
278            }
279            callback.dispose();
280        } catch (RemoteException e) {
281            return null;
282        }
283        return value;
284    }
285
286    public int getCursorCapsMode(int reqModes) {
287        int value = 0;
288        try {
289            InputContextCallback callback = InputContextCallback.getInstance();
290            mIInputContext.getCursorCapsMode(reqModes, callback.mSeq, callback);
291            synchronized (callback) {
292                callback.waitForResultLocked();
293                if (callback.mHaveValue) {
294                    value = callback.mCursorCapsMode;
295                }
296            }
297            callback.dispose();
298        } catch (RemoteException e) {
299            return 0;
300        }
301        return value;
302    }
303
304    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
305        ExtractedText value = null;
306        try {
307            InputContextCallback callback = InputContextCallback.getInstance();
308            mIInputContext.getExtractedText(request, flags, callback.mSeq, callback);
309            synchronized (callback) {
310                callback.waitForResultLocked();
311                if (callback.mHaveValue) {
312                    value = callback.mExtractedText;
313                }
314            }
315            callback.dispose();
316        } catch (RemoteException e) {
317            return null;
318        }
319        return value;
320    }
321
322    public boolean commitText(CharSequence text, int newCursorPosition) {
323        try {
324            mIInputContext.commitText(text, newCursorPosition);
325            return true;
326        } catch (RemoteException e) {
327            return false;
328        }
329    }
330
331    public boolean commitCompletion(CompletionInfo text) {
332        if (isMethodMissing(MissingMethodFlags.COMMIT_CORRECTION)) {
333            // This method is not implemented.
334            return false;
335        }
336        try {
337            mIInputContext.commitCompletion(text);
338            return true;
339        } catch (RemoteException e) {
340            return false;
341        }
342    }
343
344    public boolean commitCorrection(CorrectionInfo correctionInfo) {
345        try {
346            mIInputContext.commitCorrection(correctionInfo);
347            return true;
348        } catch (RemoteException e) {
349            return false;
350        }
351    }
352
353    public boolean setSelection(int start, int end) {
354        try {
355            mIInputContext.setSelection(start, end);
356            return true;
357        } catch (RemoteException e) {
358            return false;
359        }
360    }
361
362    public boolean performEditorAction(int actionCode) {
363        try {
364            mIInputContext.performEditorAction(actionCode);
365            return true;
366        } catch (RemoteException e) {
367            return false;
368        }
369    }
370
371    public boolean performContextMenuAction(int id) {
372        try {
373            mIInputContext.performContextMenuAction(id);
374            return true;
375        } catch (RemoteException e) {
376            return false;
377        }
378    }
379
380    public boolean setComposingRegion(int start, int end) {
381        if (isMethodMissing(MissingMethodFlags.SET_COMPOSING_REGION)) {
382            // This method is not implemented.
383            return false;
384        }
385        try {
386            mIInputContext.setComposingRegion(start, end);
387            return true;
388        } catch (RemoteException e) {
389            return false;
390        }
391    }
392
393    public boolean setComposingText(CharSequence text, int newCursorPosition) {
394        try {
395            mIInputContext.setComposingText(text, newCursorPosition);
396            return true;
397        } catch (RemoteException e) {
398            return false;
399        }
400    }
401
402    public boolean finishComposingText() {
403        try {
404            mIInputContext.finishComposingText();
405            return true;
406        } catch (RemoteException e) {
407            return false;
408        }
409    }
410
411    public boolean beginBatchEdit() {
412        try {
413            mIInputContext.beginBatchEdit();
414            return true;
415        } catch (RemoteException e) {
416            return false;
417        }
418    }
419
420    public boolean endBatchEdit() {
421        try {
422            mIInputContext.endBatchEdit();
423            return true;
424        } catch (RemoteException e) {
425            return false;
426        }
427    }
428
429    public boolean sendKeyEvent(KeyEvent event) {
430        try {
431            mIInputContext.sendKeyEvent(event);
432            return true;
433        } catch (RemoteException e) {
434            return false;
435        }
436    }
437
438    public boolean clearMetaKeyStates(int states) {
439        try {
440            mIInputContext.clearMetaKeyStates(states);
441            return true;
442        } catch (RemoteException e) {
443            return false;
444        }
445    }
446
447    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
448        try {
449            mIInputContext.deleteSurroundingText(beforeLength, afterLength);
450            return true;
451        } catch (RemoteException e) {
452            return false;
453        }
454    }
455
456    public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
457        if (isMethodMissing(MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS)) {
458            // This method is not implemented.
459            return false;
460        }
461        try {
462            mIInputContext.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
463            return true;
464        } catch (RemoteException e) {
465            return false;
466        }
467    }
468
469    public boolean reportFullscreenMode(boolean enabled) {
470        // Nothing should happen when called from input method.
471        return false;
472    }
473
474    public boolean performPrivateCommand(String action, Bundle data) {
475        try {
476            mIInputContext.performPrivateCommand(action, data);
477            return true;
478        } catch (RemoteException e) {
479            return false;
480        }
481    }
482
483    public boolean requestCursorUpdates(int cursorUpdateMode) {
484        boolean result = false;
485        if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
486            // This method is not implemented.
487            return false;
488        }
489        try {
490            InputContextCallback callback = InputContextCallback.getInstance();
491            mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback);
492            synchronized (callback) {
493                callback.waitForResultLocked();
494                if (callback.mHaveValue) {
495                    result = callback.mRequestUpdateCursorAnchorInfoResult;
496                }
497            }
498            callback.dispose();
499        } catch (RemoteException e) {
500            return false;
501        }
502        return result;
503    }
504
505    public Handler getHandler() {
506        // Nothing should happen when called from input method.
507        return null;
508    }
509
510    public void closeConnection() {
511        // Nothing should happen when called from input method.
512    }
513
514    public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
515        boolean result = false;
516        if (isMethodMissing(MissingMethodFlags.COMMIT_CONTENT)) {
517            // This method is not implemented.
518            return false;
519        }
520        try {
521            if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
522                final AbstractInputMethodService inputMethodService = mInputMethodService.get();
523                if (inputMethodService == null) {
524                    // This basically should not happen, because it's the the caller of this method.
525                    return false;
526                }
527                inputMethodService.exposeContent(inputContentInfo, this);
528            }
529
530            InputContextCallback callback = InputContextCallback.getInstance();
531            mIInputContext.commitContent(inputContentInfo, flags, opts, callback.mSeq, callback);
532            synchronized (callback) {
533                callback.waitForResultLocked();
534                if (callback.mHaveValue) {
535                    result = callback.mCommitContentResult;
536                }
537            }
538            callback.dispose();
539        } catch (RemoteException e) {
540            return false;
541        }
542        return result;
543    }
544
545    private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
546        return (mMissingMethods & methodFlag) == methodFlag;
547    }
548
549    @Override
550    public String toString() {
551        return "InputConnectionWrapper{idHash=#"
552                + Integer.toHexString(System.identityHashCode(this))
553                + " mMissingMethods="
554                + InputConnectionInspector.getMissingMethodFlagsAsString(mMissingMethods) + "}";
555    }
556}
557