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 32 import java.util.Objects; 33 34 /** 35 * A utility class to take care of boilerplate code around IPCs. 36 */ 37 public final class InputMethodPrivilegedOperations { 38 private static final String TAG = "InputMethodPrivilegedOperations"; 39 40 private static final class OpsHolder { 41 @Nullable 42 @GuardedBy("this") 43 private IInputMethodPrivilegedOperations mPrivOps; 44 45 /** 46 * Sets {@link IInputMethodPrivilegedOperations}. 47 * 48 * <p>This method can be called only once.</p> 49 * 50 * @param privOps Binder interface to be set 51 */ 52 @AnyThread set(@onNull IInputMethodPrivilegedOperations privOps)53 public synchronized void set(@NonNull IInputMethodPrivilegedOperations privOps) { 54 if (mPrivOps != null) { 55 throw new IllegalStateException( 56 "IInputMethodPrivilegedOperations must be set at most once." 57 + " privOps=" + privOps); 58 } 59 mPrivOps = privOps; 60 } 61 62 /** 63 * A simplified version of {@link android.os.Debug#getCaller()}. 64 * 65 * @return method name of the caller. 66 */ 67 @AnyThread getCallerMethodName()68 private static String getCallerMethodName() { 69 final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); 70 if (callStack.length <= 4) { 71 return "<bottom of call stack>"; 72 } 73 return callStack[4].getMethodName(); 74 } 75 76 @AnyThread 77 @Nullable getAndWarnIfNull()78 public synchronized IInputMethodPrivilegedOperations getAndWarnIfNull() { 79 if (mPrivOps == null) { 80 Log.e(TAG, getCallerMethodName() + " is ignored." 81 + " Call it within attachToken() and InputMethodService.onDestroy()"); 82 } 83 return mPrivOps; 84 } 85 } 86 private final OpsHolder mOps = new OpsHolder(); 87 88 /** 89 * Sets {@link IInputMethodPrivilegedOperations}. 90 * 91 * <p>This method can be called only once.</p> 92 * 93 * @param privOps Binder interface to be set 94 */ 95 @AnyThread set(@onNull IInputMethodPrivilegedOperations privOps)96 public void set(@NonNull IInputMethodPrivilegedOperations privOps) { 97 Objects.requireNonNull(privOps, "privOps must not be null"); 98 mOps.set(privOps); 99 } 100 101 /** 102 * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}. 103 * 104 * @param vis visibility flags 105 * @param backDisposition disposition flags 106 * @see android.inputmethodservice.InputMethodService#IME_ACTIVE 107 * @see android.inputmethodservice.InputMethodService#IME_VISIBLE 108 * @see android.inputmethodservice.InputMethodService#IME_INVISIBLE 109 * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT 110 * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_ADJUST_NOTHING 111 */ 112 @AnyThread setImeWindowStatusAsync(int vis, int backDisposition)113 public void setImeWindowStatusAsync(int vis, int backDisposition) { 114 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 115 if (ops == null) { 116 return; 117 } 118 try { 119 ops.setImeWindowStatusAsync(vis, backDisposition); 120 } catch (RemoteException e) { 121 throw e.rethrowFromSystemServer(); 122 } 123 } 124 125 /** 126 * Calls {@link IInputMethodPrivilegedOperations#reportStartInputAsync(IBinder)}. 127 * 128 * @param startInputToken {@link IBinder} token to distinguish startInput session 129 */ 130 @AnyThread reportStartInputAsync(IBinder startInputToken)131 public void reportStartInputAsync(IBinder startInputToken) { 132 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 133 if (ops == null) { 134 return; 135 } 136 try { 137 ops.reportStartInputAsync(startInputToken); 138 } catch (RemoteException e) { 139 throw e.rethrowFromSystemServer(); 140 } 141 } 142 143 /** 144 * Calls {@link IInputMethodPrivilegedOperations#createInputContentUriToken(Uri, String, 145 * IIInputContentUriTokenResultCallback)}. 146 * 147 * @param contentUri Content URI to which a temporary read permission should be granted 148 * @param packageName Indicates what package needs to have a temporary read permission 149 * @return special Binder token that should be set to 150 * {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)} 151 */ 152 @AnyThread createInputContentUriToken(Uri contentUri, String packageName)153 public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) { 154 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 155 if (ops == null) { 156 return null; 157 } 158 try { 159 final Completable.IInputContentUriToken value = 160 Completable.createIInputContentUriToken(); 161 ops.createInputContentUriToken(contentUri, packageName, ResultCallbacks.of(value)); 162 return Completable.getResult(value); 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, IVoidResultCallback)}. 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 Completable.Void value = Completable.createVoid(); 222 ops.setInputMethod(id, ResultCallbacks.of(value)); 223 Completable.getResult(value); 224 } catch (RemoteException e) { 225 throw e.rethrowFromSystemServer(); 226 } 227 } 228 229 /** 230 * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String, 231 * InputMethodSubtype, IVoidResultCallback)} 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 Completable.Void value = Completable.createVoid(); 245 ops.setInputMethodAndSubtype(id, subtype, ResultCallbacks.of(value)); 246 Completable.getResult(value); 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 * @see android.view.inputmethod.InputMethodManager#HIDE_IMPLICIT_ONLY 257 * @see android.view.inputmethod.InputMethodManager#HIDE_NOT_ALWAYS 258 */ 259 @AnyThread hideMySoftInput(int flags)260 public void hideMySoftInput(int flags) { 261 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 262 if (ops == null) { 263 return; 264 } 265 try { 266 final Completable.Void value = Completable.createVoid(); 267 ops.hideMySoftInput(flags, ResultCallbacks.of(value)); 268 Completable.getResult(value); 269 } catch (RemoteException e) { 270 throw e.rethrowFromSystemServer(); 271 } 272 } 273 274 /** 275 * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, IVoidResultCallback)} 276 * 277 * @param flags additional operating flags 278 * @see android.view.inputmethod.InputMethodManager#SHOW_IMPLICIT 279 * @see android.view.inputmethod.InputMethodManager#SHOW_FORCED 280 */ 281 @AnyThread showMySoftInput(int flags)282 public void showMySoftInput(int flags) { 283 final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); 284 if (ops == null) { 285 return; 286 } 287 try { 288 final Completable.Void value = Completable.createVoid(); 289 ops.showMySoftInput(flags, ResultCallbacks.of(value)); 290 Completable.getResult(value); 291 } catch (RemoteException e) { 292 throw e.rethrowFromSystemServer(); 293 } 294 } 295 296 /** 297 * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod( 298 * IBooleanResultCallback)} 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 Completable.Boolean value = Completable.createBoolean(); 310 ops.switchToPreviousInputMethod(ResultCallbacks.of(value)); 311 return Completable.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 Completable.Boolean value = Completable.createBoolean(); 333 ops.switchToNextInputMethod(onlyCurrentIme, ResultCallbacks.of(value)); 334 return Completable.getResult(value); 335 } catch (RemoteException e) { 336 throw e.rethrowFromSystemServer(); 337 } 338 } 339 340 /** 341 * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod( 342 * IBooleanResultCallback)} 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 Completable.Boolean value = Completable.createBoolean(); 354 ops.shouldOfferSwitchingToNextInputMethod(ResultCallbacks.of(value)); 355 return Completable.getResult(value); 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