1 /* 2 * Copyright (C) 2018 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.inputmethod; 18 19 import android.annotation.AnyThread; 20 import android.annotation.DrawableRes; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.graphics.Region; 24 import android.inputmethodservice.InputMethodService.BackDispositionMode; 25 import android.inputmethodservice.InputMethodService.ImeWindowVisibility; 26 import android.net.Uri; 27 import android.os.IBinder; 28 import android.os.RemoteException; 29 import android.util.Log; 30 import android.view.View; 31 import android.view.inputmethod.ImeTracker; 32 import android.view.inputmethod.InputMethodManager; 33 import android.view.inputmethod.InputMethodSubtype; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.infra.AndroidFuture; 37 38 import java.util.Objects; 39 40 /** 41 * A utility class to take care of boilerplate code around IPCs. 42 */ 43 public final class InputMethodPrivilegedOperations { 44 private static final String TAG = "InputMethodPrivilegedOperations"; 45 46 private static final class OpsHolder { 47 @Nullable 48 @GuardedBy("this") 49 private IInputMethodPrivilegedOperations mPrivOps; 50 51 /** 52 * Sets {@link IInputMethodPrivilegedOperations}. 53 * 54 * <p>This method can be called only once.</p> 55 * 56 * @param privOps Binder interface to be set 57 */ 58 @AnyThread set(@onNull IInputMethodPrivilegedOperations privOps)59 public synchronized void set(@NonNull IInputMethodPrivilegedOperations privOps) { 60 if (mPrivOps != null) { 61 throw new IllegalStateException( 62 "IInputMethodPrivilegedOperations must be set at most once." 63 + " privOps=" + privOps); 64 } 65 mPrivOps = privOps; 66 } 67 68 /** 69 * A simplified version of {@link android.os.Debug#getCaller()}. 70 * 71 * @return method name of the caller. 72 */ 73 @AnyThread getCallerMethodName()74 private static String getCallerMethodName() { 75 final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); 76 if (callStack.length <= 4) { 77 return "<bottom of call stack>"; 78 } 79 return callStack[4].getMethodName(); 80 } 81 82 @AnyThread 83 @Nullable getAndWarnIfNull()84 public synchronized IInputMethodPrivilegedOperations getAndWarnIfNull() { 85 if (mPrivOps == null) { 86 Log.e(TAG, getCallerMethodName() + " is ignored." 87 + " Call it within attachToken() and InputMethodService.onDestroy()"); 88 } 89 return mPrivOps; 90 } 91 } 92 private final OpsHolder mOps = new OpsHolder(); 93 94 /** 95 * Sets {@link IInputMethodPrivilegedOperations}. 96 * 97 * <p>This method can be called only once.</p> 98 * 99 * @param privOps Binder interface to be set 100 */ 101 @AnyThread set(@onNull IInputMethodPrivilegedOperations privOps)102 public void set(@NonNull IInputMethodPrivilegedOperations privOps) { 103 Objects.requireNonNull(privOps, "privOps must not be null"); 104 mOps.set(privOps); 105 } 106 107 /** 108 * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}. 109 * 110 * @param vis visibility flags 111 * @param backDisposition disposition flags 112 */ 113 @AnyThread setImeWindowStatusAsync(@meWindowVisibility int vis, @BackDispositionMode int backDisposition)114 public void setImeWindowStatusAsync(@ImeWindowVisibility int vis, 115 @BackDispositionMode int backDisposition) { 116 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 117 if (ops == null) { 118 return; 119 } 120 try { 121 ops.setImeWindowStatusAsync(vis, backDisposition); 122 } catch (RemoteException e) { 123 throw e.rethrowFromSystemServer(); 124 } 125 } 126 127 /** 128 * Calls {@link IInputMethodPrivilegedOperations#reportStartInputAsync(IBinder)}. 129 * 130 * @param startInputToken {@link IBinder} token to distinguish startInput session 131 */ 132 @AnyThread reportStartInputAsync(IBinder startInputToken)133 public void reportStartInputAsync(IBinder startInputToken) { 134 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 135 if (ops == null) { 136 return; 137 } 138 try { 139 ops.reportStartInputAsync(startInputToken); 140 } catch (RemoteException e) { 141 throw e.rethrowFromSystemServer(); 142 } 143 } 144 145 /** 146 * Calls {@link IInputMethodPrivilegedOperations#setHandwritingSurfaceNotTouchable(boolean)}. 147 * 148 * @param notTouchable {@code true} to make handwriting surface not-touchable (pass-through). 149 */ 150 @AnyThread setHandwritingSurfaceNotTouchable(boolean notTouchable)151 public void setHandwritingSurfaceNotTouchable(boolean notTouchable) { 152 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 153 if (ops == null) { 154 return; 155 } 156 try { 157 ops.setHandwritingSurfaceNotTouchable(notTouchable); 158 } catch (RemoteException e) { 159 throw e.rethrowFromSystemServer(); 160 } 161 } 162 163 164 /** 165 * Calls {@link IInputMethodPrivilegedOperations#setHandwritingTouchableRegion(Region)}. 166 * 167 * @param region {@link Region} to set handwritable. 168 */ 169 @AnyThread setHandwritingTouchableRegion(Region region)170 public void setHandwritingTouchableRegion(Region region) { 171 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 172 if (ops == null) { 173 return; 174 } 175 try { 176 ops.setHandwritingTouchableRegion(region); 177 } catch (RemoteException e) { 178 throw e.rethrowFromSystemServer(); 179 } 180 } 181 182 /** 183 * Calls {@link IInputMethodPrivilegedOperations#createInputContentUriToken(Uri, String, 184 * AndroidFuture)}. 185 * 186 * @param contentUri Content URI to which a temporary read permission should be granted 187 * @param packageName Indicates what package needs to have a temporary read permission 188 * @return special Binder token that should be set to 189 * {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)} 190 */ 191 @AnyThread createInputContentUriToken(Uri contentUri, String packageName)192 public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) { 193 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 194 if (ops == null) { 195 return null; 196 } 197 try { 198 final AndroidFuture<IBinder> future = new AndroidFuture<>(); 199 ops.createInputContentUriToken(contentUri, packageName, future); 200 return IInputContentUriToken.Stub.asInterface(CompletableFutureUtil.getResult(future)); 201 } catch (RemoteException e) { 202 // For historical reasons, this error was silently ignored. 203 // Note that the caller already logs error so we do not need additional Log.e() here. 204 // TODO(team): Check if it is safe to rethrow error here. 205 return null; 206 } 207 } 208 209 /** 210 * Calls {@link IInputMethodPrivilegedOperations#reportFullscreenModeAsync(boolean)}. 211 * 212 * @param fullscreen {@code true} if the IME enters full screen mode 213 */ 214 @AnyThread reportFullscreenModeAsync(boolean fullscreen)215 public void reportFullscreenModeAsync(boolean fullscreen) { 216 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 217 if (ops == null) { 218 return; 219 } 220 try { 221 ops.reportFullscreenModeAsync(fullscreen); 222 } catch (RemoteException e) { 223 throw e.rethrowFromSystemServer(); 224 } 225 } 226 227 /** 228 * Calls {@link IInputMethodPrivilegedOperations#updateStatusIconAsync(String, int)}. 229 * 230 * @param packageName package name from which the status icon should be loaded 231 * @param iconResId resource ID of the icon to be loaded 232 */ 233 @AnyThread updateStatusIconAsync(String packageName, @DrawableRes int iconResId)234 public void updateStatusIconAsync(String packageName, @DrawableRes int iconResId) { 235 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 236 if (ops == null) { 237 return; 238 } 239 try { 240 ops.updateStatusIconAsync(packageName, iconResId); 241 } catch (RemoteException e) { 242 throw e.rethrowFromSystemServer(); 243 } 244 } 245 246 /** 247 * Calls {@link IInputMethodPrivilegedOperations#setInputMethod(String, AndroidFuture)}. 248 * 249 * @param id IME ID of the IME to switch to 250 * @see android.view.inputmethod.InputMethodInfo#getId() 251 */ 252 @AnyThread setInputMethod(String id)253 public void setInputMethod(String id) { 254 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 255 if (ops == null) { 256 return; 257 } 258 try { 259 final AndroidFuture<Void> future = new AndroidFuture<>(); 260 ops.setInputMethod(id, future); 261 CompletableFutureUtil.getResult(future); 262 } catch (RemoteException e) { 263 throw e.rethrowFromSystemServer(); 264 } 265 } 266 267 /** 268 * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String, 269 * InputMethodSubtype, AndroidFuture)} 270 * 271 * @param id IME ID of the IME to switch to 272 * @param subtype {@link InputMethodSubtype} to switch to 273 * @see android.view.inputmethod.InputMethodInfo#getId() 274 */ 275 @AnyThread setInputMethodAndSubtype(String id, InputMethodSubtype subtype)276 public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) { 277 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 278 if (ops == null) { 279 return; 280 } 281 try { 282 final AndroidFuture<Void> future = new AndroidFuture<>(); 283 ops.setInputMethodAndSubtype(id, subtype, future); 284 CompletableFutureUtil.getResult(future); 285 } catch (RemoteException e) { 286 throw e.rethrowFromSystemServer(); 287 } 288 } 289 290 /** 291 * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput} 292 */ 293 @AnyThread hideMySoftInput(@onNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason)294 public void hideMySoftInput(@NonNull ImeTracker.Token statsToken, 295 @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason) { 296 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 297 if (ops == null) { 298 ImeTracker.forLogging().onFailed(statsToken, 299 ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 300 return; 301 } 302 ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 303 try { 304 final AndroidFuture<Void> future = new AndroidFuture<>(); 305 ops.hideMySoftInput(statsToken, flags, reason, future); 306 CompletableFutureUtil.getResult(future); 307 } catch (RemoteException e) { 308 throw e.rethrowFromSystemServer(); 309 } 310 } 311 312 /** 313 * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput} 314 */ 315 @AnyThread showMySoftInput(@onNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason)316 public void showMySoftInput(@NonNull ImeTracker.Token statsToken, 317 @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason) { 318 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 319 if (ops == null) { 320 ImeTracker.forLogging().onFailed(statsToken, 321 ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 322 return; 323 } 324 ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 325 try { 326 final AndroidFuture<Void> future = new AndroidFuture<>(); 327 ops.showMySoftInput(statsToken, flags, reason, future); 328 CompletableFutureUtil.getResult(future); 329 } catch (RemoteException e) { 330 throw e.rethrowFromSystemServer(); 331 } 332 } 333 334 /** 335 * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod(AndroidFuture)} 336 * 337 * @return {@code true} if handled 338 */ 339 @AnyThread switchToPreviousInputMethod()340 public boolean switchToPreviousInputMethod() { 341 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 342 if (ops == null) { 343 return false; 344 } 345 try { 346 final AndroidFuture<Boolean> value = new AndroidFuture<>(); 347 ops.switchToPreviousInputMethod(value); 348 return CompletableFutureUtil.getResult(value); 349 } catch (RemoteException e) { 350 throw e.rethrowFromSystemServer(); 351 } 352 } 353 354 /** 355 * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean, 356 * AndroidFuture)} 357 * 358 * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same 359 * IME 360 * @return {@code true} if handled 361 */ 362 @AnyThread switchToNextInputMethod(boolean onlyCurrentIme)363 public boolean switchToNextInputMethod(boolean onlyCurrentIme) { 364 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 365 if (ops == null) { 366 return false; 367 } 368 try { 369 final AndroidFuture<Boolean> future = new AndroidFuture<>(); 370 ops.switchToNextInputMethod(onlyCurrentIme, future); 371 return CompletableFutureUtil.getResult(future); 372 } catch (RemoteException e) { 373 throw e.rethrowFromSystemServer(); 374 } 375 } 376 377 /** 378 * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod( 379 * AndroidFuture)} 380 * 381 * @return {@code true} if the IEM should offer a way to globally switch IME 382 */ 383 @AnyThread shouldOfferSwitchingToNextInputMethod()384 public boolean shouldOfferSwitchingToNextInputMethod() { 385 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 386 if (ops == null) { 387 return false; 388 } 389 try { 390 final AndroidFuture<Boolean> future = new AndroidFuture<>(); 391 ops.shouldOfferSwitchingToNextInputMethod(future); 392 return CompletableFutureUtil.getResult(future); 393 } catch (RemoteException e) { 394 throw e.rethrowFromSystemServer(); 395 } 396 } 397 398 /** 399 * Calls {@link IInputMethodPrivilegedOperations#onImeSwitchButtonClickFromClient(int)} 400 */ 401 @AnyThread onImeSwitchButtonClickFromClient(int displayId)402 public void onImeSwitchButtonClickFromClient(int displayId) { 403 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 404 if (ops == null) { 405 return; 406 } 407 try { 408 ops.onImeSwitchButtonClickFromClient(displayId); 409 } catch (RemoteException e) { 410 throw e.rethrowFromSystemServer(); 411 } 412 } 413 414 /** 415 * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()} 416 */ 417 @AnyThread notifyUserActionAsync()418 public void notifyUserActionAsync() { 419 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 420 if (ops == null) { 421 return; 422 } 423 try { 424 ops.notifyUserActionAsync(); 425 } catch (RemoteException e) { 426 throw e.rethrowFromSystemServer(); 427 } 428 } 429 430 /** 431 * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean, 432 * ImeTracker.Token)}. 433 * 434 * @param showOrHideInputToken placeholder token that maps to window requesting 435 * {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or 436 * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder, 437 * int)} 438 * @param setVisible {@code true} to set IME visible, else hidden. 439 * @param statsToken the token tracking the current IME request. 440 */ 441 @AnyThread applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, @NonNull ImeTracker.Token statsToken)442 public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, 443 @NonNull ImeTracker.Token statsToken) { 444 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 445 if (ops == null) { 446 ImeTracker.forLogging().onFailed(statsToken, 447 ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 448 return; 449 } 450 ImeTracker.forLogging().onProgress(statsToken, 451 ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); 452 try { 453 ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken); 454 } catch (RemoteException e) { 455 throw e.rethrowFromSystemServer(); 456 } 457 } 458 459 /** 460 * Calls {@link IInputMethodPrivilegedOperations#onStylusHandwritingReady(int, int)} 461 */ 462 @AnyThread onStylusHandwritingReady(int requestId, int pid)463 public void onStylusHandwritingReady(int requestId, int pid) { 464 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 465 if (ops == null) { 466 return; 467 } 468 try { 469 ops.onStylusHandwritingReady(requestId, pid); 470 } catch (RemoteException e) { 471 throw e.rethrowFromSystemServer(); 472 } 473 } 474 475 /** 476 * IME notifies that the current handwriting session should be closed. 477 * @param requestId 478 */ 479 @AnyThread resetStylusHandwriting(int requestId)480 public void resetStylusHandwriting(int requestId) { 481 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 482 if (ops == null) { 483 return; 484 } 485 try { 486 ops.resetStylusHandwriting(requestId); 487 } catch (RemoteException e) { 488 throw e.rethrowFromSystemServer(); 489 } 490 } 491 492 /** 493 * Calls {@link IInputMethodPrivilegedOperations#switchKeyboardLayoutAsync(int)}. 494 */ 495 @AnyThread switchKeyboardLayoutAsync(int direction)496 public void switchKeyboardLayoutAsync(int direction) { 497 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 498 if (ops == null) { 499 return; 500 } 501 try { 502 ops.switchKeyboardLayoutAsync(direction); 503 } catch (RemoteException e) { 504 throw e.rethrowFromSystemServer(); 505 } 506 } 507 } 508