Home | History | Annotate | Download | only in view
      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 
     17 package com.android.internal.view;
     18 
     19 import android.annotation.AnyThread;
     20 import android.annotation.BinderThread;
     21 import android.annotation.NonNull;
     22 import android.inputmethodservice.AbstractInputMethodService;
     23 import android.os.Bundle;
     24 import android.os.Handler;
     25 import android.os.RemoteException;
     26 import android.os.SystemClock;
     27 import android.util.Log;
     28 import android.view.KeyEvent;
     29 import android.view.inputmethod.CompletionInfo;
     30 import android.view.inputmethod.CorrectionInfo;
     31 import android.view.inputmethod.ExtractedText;
     32 import android.view.inputmethod.ExtractedTextRequest;
     33 import android.view.inputmethod.InputConnection;
     34 import android.view.inputmethod.InputConnectionInspector;
     35 import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
     36 import android.view.inputmethod.InputContentInfo;
     37 
     38 import java.lang.ref.WeakReference;
     39 import java.util.concurrent.atomic.AtomicBoolean;
     40 
     41 public 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