From 2422bcffe2351789dc80be68b6f6d5515dc0fa11 Mon Sep 17 00:00:00 2001 From: Taran Singh Date: Thu, 27 Oct 2022 19:14:52 +0000 Subject: [PATCH] Introduce IMM#getCurrentInputMethodInfo(userId) introduce an API for apps to query currently selected aka default input method for current or a given user. Existing API getEnabledInputMethodList() builds list for allenabled IMEs, which is inefficient and redundant for several cases. Bug: 255799034 Test: atest CtsInputMethodServiceHostTestCases:MultiUserTest Change-Id: I60a0f67bf7d261d3a4a733adcb8a022ceac6e1db --- core/api/current.txt | 1 + core/api/system-current.txt | 8 +++ .../IInputMethodManagerGlobalInvoker.java | 15 +++++ .../view/inputmethod/InputMethodInfo.java | 22 +++++++ .../view/inputmethod/InputMethodManager.java | 33 ++++++++++ .../internal/view/IInputMethodManager.aidl | 5 ++ .../InputMethodManagerService.java | 60 +++++++++++++++++++ .../server/inputmethod/InputMethodUtils.java | 18 +++++- 8 files changed, 161 insertions(+), 1 deletion(-) diff --git a/core/api/current.txt b/core/api/current.txt index 623f210b83268..22a9aef3d703f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -54250,6 +54250,7 @@ package android.view.inputmethod { public final class InputMethodManager { method public void dispatchKeyEventFromInputMethod(@Nullable android.view.View, @NonNull android.view.KeyEvent); method public void displayCompletions(android.view.View, android.view.inputmethod.CompletionInfo[]); + method @Nullable public android.view.inputmethod.InputMethodInfo getCurrentInputMethodInfo(); method @Nullable public android.view.inputmethod.InputMethodSubtype getCurrentInputMethodSubtype(); method @NonNull public java.util.List getEnabledInputMethodList(); method @NonNull public java.util.List getEnabledInputMethodSubtypeList(@Nullable android.view.inputmethod.InputMethodInfo, boolean); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 992e524d7edf1..3a94f5c96ca01 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16307,6 +16307,14 @@ package android.view.displayhash { } +package android.view.inputmethod { + + public final class InputMethodManager { + method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.view.inputmethod.InputMethodInfo getCurrentInputMethodInfoAsUser(@NonNull android.os.UserHandle); + } + +} + package android.view.translation { public final class TranslationCapability implements android.os.Parcelable { diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index 6eae63afcba7b..5db736d510469 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -210,6 +210,21 @@ final class IInputMethodManagerGlobalInvoker { } } + @AnyThread + @Nullable + @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) + static InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return null; + } + try { + return service.getCurrentInputMethodInfoAsUser(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @AnyThread @NonNull @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index d7c18462077d6..52b9755100d59 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -331,6 +331,28 @@ public final class InputMethodInfo implements Parcelable { mIsVrOnly = isVrOnly; } + /** + * @hide + */ + public InputMethodInfo(InputMethodInfo source) { + mId = source.mId; + mSettingsActivityName = source.mSettingsActivityName; + mIsDefaultResId = source.mIsDefaultResId; + mIsAuxIme = source.mIsAuxIme; + mSupportsSwitchingToNextInputMethod = source.mSupportsSwitchingToNextInputMethod; + mInlineSuggestionsEnabled = source.mInlineSuggestionsEnabled; + mSupportsInlineSuggestionsWithTouchExploration = + source.mSupportsInlineSuggestionsWithTouchExploration; + mSuppressesSpellChecker = source.mSuppressesSpellChecker; + mShowInInputMethodPicker = source.mShowInInputMethodPicker; + mIsVrOnly = source.mIsVrOnly; + mService = source.mService; + mSubtypes = source.mSubtypes; + mHandledConfigChanges = source.mHandledConfigChanges; + mSupportsStylusHandwriting = source.mSupportsStylusHandwriting; + mForceDefault = source.mForceDefault; + } + InputMethodInfo(Parcel source) { mId = source.readString(); mSettingsActivityName = source.readString(); diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index ee31fd5763b77..5382f3541b95e 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -42,6 +42,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UiThread; @@ -1594,6 +1596,37 @@ public final class InputMethodManager { return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, directBootAwareness); } + /** + * Returns the {@link InputMethodInfo} of the currently selected input method (for the process's + * user). + * + *

On multi user environment, this API returns a result for the calling process user.

+ */ + @Nullable + public InputMethodInfo getCurrentInputMethodInfo() { + // We intentionally do not use UserHandle.getCallingUserId() here because for system + // services InputMethodManagerInternal.getCurrentInputMethodInfoForUser() should be used + // instead. + return IInputMethodManagerGlobalInvoker.getCurrentInputMethodInfoAsUser( + UserHandle.myUserId()); + } + + /** + * Returns the {@link InputMethodInfo} for currently selected input method for the given user. + * + * @param user user to query. + * @hide + */ + @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL) + @Nullable + @SystemApi + @SuppressLint("UserHandle") + public InputMethodInfo getCurrentInputMethodInfoAsUser(@NonNull UserHandle user) { + Objects.requireNonNull(user); + return IInputMethodManagerGlobalInvoker.getCurrentInputMethodInfoAsUser( + user.getIdentifier()); + } + /** * Returns the list of enabled input methods. * diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 00bc3f2ae5308..5248e0871ff4e 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -36,6 +36,11 @@ interface IInputMethodManager { void addClient(in IInputMethodClient client, in IRemoteInputConnection inputmethod, int untrustedDisplayId); + // TODO: Use ParceledListSlice instead + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") + InputMethodInfo getCurrentInputMethodInfoAsUser(int userId); + // TODO: Use ParceledListSlice instead @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)") diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 080d5829f9d91..86f3a8049b6e0 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1993,6 +1993,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return true; } + @BinderThread + @Nullable + @Override + public InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) { + if (UserHandle.getCallingUserId() != userId) { + mContext.enforceCallingPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); + } + synchronized (ImfLock.class) { + return queryDefaultInputMethodForUserIdLocked(userId); + } + } + @BinderThread @NonNull @Override @@ -5503,6 +5516,53 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return mCurrentSubtype; } + /** + * Returns the default {@link InputMethodInfo} for the specific userId. + * @param userId user ID to query. + */ + @GuardedBy("ImfLock.class") + private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { + final String imeId = mSettings.getSelectedInputMethodForUser(userId); + if (TextUtils.isEmpty(imeId)) { + Slog.e(TAG, "No default input method found for userId " + userId); + return null; + } + + InputMethodInfo curInputMethodInfo; + if (userId == mSettings.getCurrentUserId() + && (curInputMethodInfo = mMethodMap.get(imeId)) != null) { + // clone the InputMethodInfo before returning. + return new InputMethodInfo(curInputMethodInfo); + } + + final ArrayMap> additionalSubtypeMap = new ArrayMap<>(); + AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + Context userAwareContext = + mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */); + + final int flags = PackageManager.GET_META_DATA + | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + | PackageManager.MATCH_DIRECT_BOOT_AUTO; + final List services = + userAwareContext.getPackageManager().queryIntentServicesAsUser( + new Intent(InputMethod.SERVICE_INTERFACE), + PackageManager.ResolveInfoFlags.of(flags), + userId); + for (ResolveInfo ri : services) { + final String imeIdResolved = InputMethodInfo.computeId(ri); + if (imeId.equals(imeIdResolved)) { + try { + return new InputMethodInfo( + userAwareContext, ri, additionalSubtypeMap.get(imeId)); + } catch (Exception e) { + Slog.wtf(TAG, "Unable to load input method " + imeId, e); + } + } + } + // we didn't find the InputMethodInfo for imeId. This shouldn't happen. + Slog.e(TAG, "Error while locating input method info for imeId: " + imeId); + return null; + } private ArrayMap queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap methodMap = new ArrayMap<>(); final ArrayList methodList = new ArrayList<>(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index be99bfbc2bc91..559eb5341632b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -330,11 +330,17 @@ final class InputMethodUtils { @Nullable private String getString(@NonNull String key, @Nullable String defaultValue) { + return getStringForUser(key, defaultValue, mCurrentUserId); + } + + @Nullable + private String getStringForUser( + @NonNull String key, @Nullable String defaultValue, @UserIdInt int userId) { final String result; if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) { result = mCopyOnWriteDataStore.get(key); } else { - result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId); + result = Settings.Secure.getStringForUser(mResolver, key, userId); } return result != null ? result : defaultValue; } @@ -741,6 +747,16 @@ final class InputMethodUtils { return imi; } + @Nullable + String getSelectedInputMethodForUser(@UserIdInt int userId) { + final String imi = + getStringForUser(Settings.Secure.DEFAULT_INPUT_METHOD, null, userId); + if (DEBUG) { + Slog.d(TAG, "getSelectedInputMethodForUserStr: " + imi); + } + return imi; + } + void putDefaultVoiceInputMethod(String imeId) { if (DEBUG) { Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);