474 lines
17 KiB
Java
474 lines
17 KiB
Java
![]() |
/*
|
||
|
* 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}.
|
||
|
*
|
||
|
* <p>This method can be called only once.</p>
|
||
|
*
|
||
|
* @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 "<bottom of call stack>";
|
||
|
}
|
||
|
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}.
|
||
|
*
|
||
|
* <p>This method can be called only once.</p>
|
||
|
*
|
||
|
* @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<IBinder> 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<Void> 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<Void> 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<Void> 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<Void> 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<Boolean> 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<Boolean> 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<Boolean> 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();
|
||
|
}
|
||
|
}
|
||
|
}
|