diff --git a/core/api/current.txt b/core/api/current.txt index 073bcf9e5d280..84274cb507674 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -54830,6 +54830,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 aa7087e7a22f2..0119cbde2ecf1 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16633,6 +16633,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 f08f61f99696d..966026198a211 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -214,6 +214,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 b7da73247efd5..229cc02b793e8 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -339,6 +339,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 c1b6cda2cc673..99bd02de62666 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -43,6 +43,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; @@ -1597,6 +1599,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 2106426c6c9b4..9116cb3ec9fa2 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -37,6 +37,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 a94c90c8ee63e..19108a883d2cd 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1957,6 +1957,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 @@ -5288,6 +5301,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);