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#createInputContentUriToken(Uri, String, 148 * AndroidFuture)}. 149 * 150 * @param contentUri Content URI to which a temporary read permission should be granted 151 * @param packageName Indicates what package needs to have a temporary read permission 152 * @return special Binder token that should be set to 153 * {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)} 154 */ 155 @AnyThread createInputContentUriToken(Uri contentUri, String packageName)156 public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) { 157 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 158 if (ops == null) { 159 return null; 160 } 161 try { 162 final AndroidFuture<IBinder> future = new AndroidFuture<>(); 163 ops.createInputContentUriToken(contentUri, packageName, future); 164 return IInputContentUriToken.Stub.asInterface(CompletableFutureUtil.getResult(future)); 165 } catch (RemoteException e) { 166 // For historical reasons, this error was silently ignored. 167 // Note that the caller already logs error so we do not need additional Log.e() here. 168 // TODO(team): Check if it is safe to rethrow error here. 169 return null; 170 } 171 } 172 173 /** 174 * Calls {@link IInputMethodPrivilegedOperations#reportFullscreenModeAsync(boolean)}. 175 * 176 * @param fullscreen {@code true} if the IME enters full screen mode 177 */ 178 @AnyThread reportFullscreenModeAsync(boolean fullscreen)179 public void reportFullscreenModeAsync(boolean fullscreen) { 180 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 181 if (ops == null) { 182 return; 183 } 184 try { 185 ops.reportFullscreenModeAsync(fullscreen); 186 } catch (RemoteException e) { 187 throw e.rethrowFromSystemServer(); 188 } 189 } 190 191 /** 192 * Calls {@link IInputMethodPrivilegedOperations#updateStatusIconAsync(String, int)}. 193 * 194 * @param packageName package name from which the status icon should be loaded 195 * @param iconResId resource ID of the icon to be loaded 196 */ 197 @AnyThread updateStatusIconAsync(String packageName, @DrawableRes int iconResId)198 public void updateStatusIconAsync(String packageName, @DrawableRes int iconResId) { 199 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 200 if (ops == null) { 201 return; 202 } 203 try { 204 ops.updateStatusIconAsync(packageName, iconResId); 205 } catch (RemoteException e) { 206 throw e.rethrowFromSystemServer(); 207 } 208 } 209 210 /** 211 * Calls {@link IInputMethodPrivilegedOperations#setInputMethod(String, AndroidFuture)}. 212 * 213 * @param id IME ID of the IME to switch to 214 * @see android.view.inputmethod.InputMethodInfo#getId() 215 */ 216 @AnyThread setInputMethod(String id)217 public void setInputMethod(String id) { 218 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 219 if (ops == null) { 220 return; 221 } 222 try { 223 final AndroidFuture<Void> future = new AndroidFuture<>(); 224 ops.setInputMethod(id, future); 225 CompletableFutureUtil.getResult(future); 226 } catch (RemoteException e) { 227 throw e.rethrowFromSystemServer(); 228 } 229 } 230 231 /** 232 * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String, 233 * InputMethodSubtype, AndroidFuture)} 234 * 235 * @param id IME ID of the IME to switch to 236 * @param subtype {@link InputMethodSubtype} to switch to 237 * @see android.view.inputmethod.InputMethodInfo#getId() 238 */ 239 @AnyThread setInputMethodAndSubtype(String id, InputMethodSubtype subtype)240 public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) { 241 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 242 if (ops == null) { 243 return; 244 } 245 try { 246 final AndroidFuture<Void> future = new AndroidFuture<>(); 247 ops.setInputMethodAndSubtype(id, subtype, future); 248 CompletableFutureUtil.getResult(future); 249 } catch (RemoteException e) { 250 throw e.rethrowFromSystemServer(); 251 } 252 } 253 254 /** 255 * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)} 256 * 257 * @param reason the reason to hide soft input 258 */ 259 @AnyThread hideMySoftInput(@nputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason)260 public void hideMySoftInput(@InputMethodManager.HideFlags int flags, 261 @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 @AnyThread showMySoftInput(@nputMethodManager.ShowFlags int flags)279 public void showMySoftInput(@InputMethodManager.ShowFlags int flags) { 280 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 281 if (ops == null) { 282 return; 283 } 284 try { 285 final AndroidFuture<Void> future = new AndroidFuture<>(); 286 ops.showMySoftInput(flags, future); 287 CompletableFutureUtil.getResult(future); 288 } catch (RemoteException e) { 289 throw e.rethrowFromSystemServer(); 290 } 291 } 292 293 /** 294 * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod(AndroidFuture)} 295 * 296 * @return {@code true} if handled 297 */ 298 @AnyThread switchToPreviousInputMethod()299 public boolean switchToPreviousInputMethod() { 300 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 301 if (ops == null) { 302 return false; 303 } 304 try { 305 final AndroidFuture<Boolean> value = new AndroidFuture<>(); 306 ops.switchToPreviousInputMethod(value); 307 return CompletableFutureUtil.getResult(value); 308 } catch (RemoteException e) { 309 throw e.rethrowFromSystemServer(); 310 } 311 } 312 313 /** 314 * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean, 315 * AndroidFuture)} 316 * 317 * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same 318 * IME 319 * @return {@code true} if handled 320 */ 321 @AnyThread switchToNextInputMethod(boolean onlyCurrentIme)322 public boolean switchToNextInputMethod(boolean onlyCurrentIme) { 323 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 324 if (ops == null) { 325 return false; 326 } 327 try { 328 final AndroidFuture<Boolean> future = new AndroidFuture<>(); 329 ops.switchToNextInputMethod(onlyCurrentIme, future); 330 return CompletableFutureUtil.getResult(future); 331 } catch (RemoteException e) { 332 throw e.rethrowFromSystemServer(); 333 } 334 } 335 336 /** 337 * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod( 338 * AndroidFuture)} 339 * 340 * @return {@code true} if the IEM should offer a way to globally switch IME 341 */ 342 @AnyThread shouldOfferSwitchingToNextInputMethod()343 public boolean shouldOfferSwitchingToNextInputMethod() { 344 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 345 if (ops == null) { 346 return false; 347 } 348 try { 349 final AndroidFuture<Boolean> future = new AndroidFuture<>(); 350 ops.shouldOfferSwitchingToNextInputMethod(future); 351 return CompletableFutureUtil.getResult(future); 352 } catch (RemoteException e) { 353 throw e.rethrowFromSystemServer(); 354 } 355 } 356 357 /** 358 * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()} 359 */ 360 @AnyThread notifyUserActionAsync()361 public void notifyUserActionAsync() { 362 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 363 if (ops == null) { 364 return; 365 } 366 try { 367 ops.notifyUserActionAsync(); 368 } catch (RemoteException e) { 369 throw e.rethrowFromSystemServer(); 370 } 371 } 372 373 /** 374 * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean, 375 * ImeTracker.Token)}. 376 * 377 * @param showOrHideInputToken placeholder token that maps to window requesting 378 * {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or 379 * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder, 380 * int)} 381 * @param setVisible {@code true} to set IME visible, else hidden. 382 * @param statsToken the token tracking the current IME request or {@code null} otherwise. 383 */ 384 @AnyThread applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, @Nullable ImeTracker.Token statsToken)385 public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, 386 @Nullable ImeTracker.Token statsToken) { 387 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 388 if (ops == null) { 389 ImeTracker.forLogging().onFailed(statsToken, 390 ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER); 391 return; 392 } 393 ImeTracker.forLogging().onProgress(statsToken, 394 ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER); 395 try { 396 ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken); 397 } catch (RemoteException e) { 398 throw e.rethrowFromSystemServer(); 399 } 400 } 401 402 /** 403 * Calls {@link IInputMethodPrivilegedOperations#onStylusHandwritingReady(int, int)} 404 */ 405 @AnyThread onStylusHandwritingReady(int requestId, int pid)406 public void onStylusHandwritingReady(int requestId, int pid) { 407 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 408 if (ops == null) { 409 return; 410 } 411 try { 412 ops.onStylusHandwritingReady(requestId, pid); 413 } catch (RemoteException e) { 414 throw e.rethrowFromSystemServer(); 415 } 416 } 417 418 /** 419 * IME notifies that the current handwriting session should be closed. 420 * @param requestId 421 */ 422 @AnyThread resetStylusHandwriting(int requestId)423 public void resetStylusHandwriting(int requestId) { 424 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 425 if (ops == null) { 426 return; 427 } 428 try { 429 ops.resetStylusHandwriting(requestId); 430 } catch (RemoteException e) { 431 throw e.rethrowFromSystemServer(); 432 } 433 } 434 } 435