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