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