/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.inputmethod; import android.annotation.AnyThread; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.view.View; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AndroidFuture; import java.util.Objects; /** * A utility class to take care of boilerplate code around IPCs. */ public final class InputMethodPrivilegedOperations { private static final String TAG = "InputMethodPrivilegedOperations"; private static final class OpsHolder { @Nullable @GuardedBy("this") private IInputMethodPrivilegedOperations mPrivOps; /** * Sets {@link IInputMethodPrivilegedOperations}. * *

This method can be called only once.

* * @param privOps Binder interface to be set */ @AnyThread public synchronized void set(@NonNull IInputMethodPrivilegedOperations privOps) { if (mPrivOps != null) { throw new IllegalStateException( "IInputMethodPrivilegedOperations must be set at most once." + " privOps=" + privOps); } mPrivOps = privOps; } /** * A simplified version of {@link android.os.Debug#getCaller()}. * * @return method name of the caller. */ @AnyThread private static String getCallerMethodName() { final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); if (callStack.length <= 4) { return ""; } return callStack[4].getMethodName(); } @AnyThread @Nullable public synchronized IInputMethodPrivilegedOperations getAndWarnIfNull() { if (mPrivOps == null) { Log.e(TAG, getCallerMethodName() + " is ignored." + " Call it within attachToken() and InputMethodService.onDestroy()"); } return mPrivOps; } } private final OpsHolder mOps = new OpsHolder(); /** * Sets {@link IInputMethodPrivilegedOperations}. * *

This method can be called only once.

* * @param privOps Binder interface to be set */ @AnyThread public void set(@NonNull IInputMethodPrivilegedOperations privOps) { Objects.requireNonNull(privOps, "privOps must not be null"); mOps.set(privOps); } /** * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}. * * @param vis visibility flags * @param backDisposition disposition flags * @see android.inputmethodservice.InputMethodService#IME_ACTIVE * @see android.inputmethodservice.InputMethodService#IME_VISIBLE * @see android.inputmethodservice.InputMethodService#IME_INVISIBLE * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_ADJUST_NOTHING */ @AnyThread public void setImeWindowStatusAsync(int vis, int backDisposition) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.setImeWindowStatusAsync(vis, backDisposition); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#reportStartInputAsync(IBinder)}. * * @param startInputToken {@link IBinder} token to distinguish startInput session */ @AnyThread public void reportStartInputAsync(IBinder startInputToken) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.reportStartInputAsync(startInputToken); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#setHandwritingSurfaceNotTouchable(boolean)}. * * @param notTouchable {@code true} to make handwriting surface not-touchable (pass-through). */ @AnyThread public void setHandwritingSurfaceNotTouchable(boolean notTouchable) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.setHandwritingSurfaceNotTouchable(notTouchable); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#createInputContentUriToken(Uri, String, * AndroidFuture)}. * * @param contentUri Content URI to which a temporary read permission should be granted * @param packageName Indicates what package needs to have a temporary read permission * @return special Binder token that should be set to * {@link android.view.inputmethod.InputContentInfo#setUriToken(IInputContentUriToken)} */ @AnyThread public IInputContentUriToken createInputContentUriToken(Uri contentUri, String packageName) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return null; } try { final AndroidFuture future = new AndroidFuture<>(); ops.createInputContentUriToken(contentUri, packageName, future); return IInputContentUriToken.Stub.asInterface(CompletableFutureUtil.getResult(future)); } catch (RemoteException e) { // For historical reasons, this error was silently ignored. // Note that the caller already logs error so we do not need additional Log.e() here. // TODO(team): Check if it is safe to rethrow error here. return null; } } /** * Calls {@link IInputMethodPrivilegedOperations#reportFullscreenModeAsync(boolean)}. * * @param fullscreen {@code true} if the IME enters full screen mode */ @AnyThread public void reportFullscreenModeAsync(boolean fullscreen) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.reportFullscreenModeAsync(fullscreen); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#updateStatusIconAsync(String, int)}. * * @param packageName package name from which the status icon should be loaded * @param iconResId resource ID of the icon to be loaded */ @AnyThread public void updateStatusIconAsync(String packageName, @DrawableRes int iconResId) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.updateStatusIconAsync(packageName, iconResId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#setInputMethod(String, AndroidFuture)}. * * @param id IME ID of the IME to switch to * @see android.view.inputmethod.InputMethodInfo#getId() */ @AnyThread public void setInputMethod(String id) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { final AndroidFuture future = new AndroidFuture<>(); ops.setInputMethod(id, future); CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#setInputMethodAndSubtype(String, * InputMethodSubtype, AndroidFuture)} * * @param id IME ID of the IME to switch to * @param subtype {@link InputMethodSubtype} to switch to * @see android.view.inputmethod.InputMethodInfo#getId() */ @AnyThread public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { final AndroidFuture future = new AndroidFuture<>(); ops.setInputMethodAndSubtype(id, subtype, future); CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput} */ @AnyThread public void hideMySoftInput(@NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); try { final AndroidFuture future = new AndroidFuture<>(); ops.hideMySoftInput(statsToken, flags, reason, future); CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput} */ @AnyThread public void showMySoftInput(@NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); try { final AndroidFuture future = new AndroidFuture<>(); ops.showMySoftInput(statsToken, flags, reason, future); CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#switchToPreviousInputMethod(AndroidFuture)} * * @return {@code true} if handled */ @AnyThread public boolean switchToPreviousInputMethod() { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return false; } try { final AndroidFuture value = new AndroidFuture<>(); ops.switchToPreviousInputMethod(value); return CompletableFutureUtil.getResult(value); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean, * AndroidFuture)} * * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same * IME * @return {@code true} if handled */ @AnyThread public boolean switchToNextInputMethod(boolean onlyCurrentIme) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return false; } try { final AndroidFuture future = new AndroidFuture<>(); ops.switchToNextInputMethod(onlyCurrentIme, future); return CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#shouldOfferSwitchingToNextInputMethod( * AndroidFuture)} * * @return {@code true} if the IEM should offer a way to globally switch IME */ @AnyThread public boolean shouldOfferSwitchingToNextInputMethod() { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return false; } try { final AndroidFuture future = new AndroidFuture<>(); ops.shouldOfferSwitchingToNextInputMethod(future); return CompletableFutureUtil.getResult(future); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()} */ @AnyThread public void notifyUserActionAsync() { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.notifyUserActionAsync(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean, * ImeTracker.Token)}. * * @param showOrHideInputToken placeholder token that maps to window requesting * {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder, * int)} * @param setVisible {@code true} to set IME visible, else hidden. * @param statsToken the token tracking the current IME request. */ @AnyThread public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible, @NonNull ImeTracker.Token statsToken) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); return; } ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_PRIVILEGED_OPERATIONS); try { ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#onStylusHandwritingReady(int, int)} */ @AnyThread public void onStylusHandwritingReady(int requestId, int pid) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.onStylusHandwritingReady(requestId, pid); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * IME notifies that the current handwriting session should be closed. * @param requestId */ @AnyThread public void resetStylusHandwriting(int requestId) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.resetStylusHandwriting(requestId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Calls {@link IInputMethodPrivilegedOperations#switchKeyboardLayoutAsync(int)}. */ @AnyThread public void switchKeyboardLayoutAsync(int direction) { final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull(); if (ops == null) { return; } try { ops.switchKeyboardLayoutAsync(direction); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } }