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