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