Merge "VDM IME 2/n: setInputMethodComponent API" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 777bca2..a1465df 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3248,6 +3248,7 @@
method @Deprecated public int getDefaultNavigationPolicy();
method public int getDevicePolicy(int);
method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @Nullable public android.content.ComponentName getHomeComponent();
+ method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @Nullable public android.content.ComponentName getInputMethodComponent();
method public int getLockState();
method @Nullable public String getName();
method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
@@ -3281,6 +3282,7 @@
method @Deprecated @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName);
+ method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setInputMethodComponent(@Nullable android.content.ComponentName);
method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setUsersWithMatchingAccounts(@NonNull java.util.Set<android.os.UserHandle>);
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 0d73e44..0253ddd 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -258,6 +258,7 @@
// Mapping of @PolicyType to @DevicePolicy
@NonNull private final SparseIntArray mDevicePolicies;
@Nullable private final ComponentName mHomeComponent;
+ @Nullable private final ComponentName mInputMethodComponent;
@NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
@Nullable private final IVirtualSensorCallback mVirtualSensorCallback;
private final int mAudioPlaybackSessionId;
@@ -273,6 +274,7 @@
@Nullable String name,
@NonNull SparseIntArray devicePolicies,
@Nullable ComponentName homeComponent,
+ @Nullable ComponentName inputMethodComponent,
@NonNull List<VirtualSensorConfig> virtualSensorConfigs,
@Nullable IVirtualSensorCallback virtualSensorCallback,
int audioPlaybackSessionId,
@@ -289,6 +291,7 @@
mName = name;
mDevicePolicies = Objects.requireNonNull(devicePolicies);
mHomeComponent = homeComponent;
+ mInputMethodComponent = inputMethodComponent;
mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
mVirtualSensorCallback = virtualSensorCallback;
mAudioPlaybackSessionId = audioPlaybackSessionId;
@@ -312,6 +315,7 @@
mAudioPlaybackSessionId = parcel.readInt();
mAudioRecordingSessionId = parcel.readInt();
mHomeComponent = parcel.readTypedObject(ComponentName.CREATOR);
+ mInputMethodComponent = parcel.readTypedObject(ComponentName.CREATOR);
}
/**
@@ -336,6 +340,18 @@
}
/**
+ * Returns the custom component used as input method on all displays owned by this virtual
+ * device.
+ *
+ * @see Builder#setInputMethodComponent
+ */
+ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_IME)
+ @Nullable
+ public ComponentName getInputMethodComponent() {
+ return mInputMethodComponent;
+ }
+
+ /**
* Returns the user handles with matching managed accounts on the remote device to which
* this virtual device is streaming.
*
@@ -532,6 +548,7 @@
dest.writeInt(mAudioPlaybackSessionId);
dest.writeInt(mAudioRecordingSessionId);
dest.writeTypedObject(mHomeComponent, flags);
+ dest.writeTypedObject(mInputMethodComponent, flags);
}
@Override
@@ -563,6 +580,8 @@
&& Objects.equals(mActivityPolicyExemptions, that.mActivityPolicyExemptions)
&& mDefaultActivityPolicy == that.mDefaultActivityPolicy
&& Objects.equals(mName, that.mName)
+ && Objects.equals(mHomeComponent, that.mHomeComponent)
+ && Objects.equals(mInputMethodComponent, that.mInputMethodComponent)
&& mAudioPlaybackSessionId == that.mAudioPlaybackSessionId
&& mAudioRecordingSessionId == that.mAudioRecordingSessionId;
}
@@ -572,7 +591,8 @@
int hashCode = Objects.hash(
mLockState, mUsersWithMatchingAccounts, mCrossTaskNavigationExemptions,
mDefaultNavigationPolicy, mActivityPolicyExemptions, mDefaultActivityPolicy, mName,
- mDevicePolicies, mHomeComponent, mAudioPlaybackSessionId, mAudioRecordingSessionId);
+ mDevicePolicies, mHomeComponent, mInputMethodComponent, mAudioPlaybackSessionId,
+ mAudioRecordingSessionId);
for (int i = 0; i < mDevicePolicies.size(); i++) {
hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -593,6 +613,7 @@
+ " mName=" + mName
+ " mDevicePolicies=" + mDevicePolicies
+ " mHomeComponent=" + mHomeComponent
+ + " mInputMethodComponent=" + mInputMethodComponent
+ " mAudioPlaybackSessionId=" + mAudioPlaybackSessionId
+ " mAudioRecordingSessionId=" + mAudioRecordingSessionId
+ ")";
@@ -612,6 +633,8 @@
pw.println(prefix + "mActivityPolicyExemptions=" + mActivityPolicyExemptions);
pw.println(prefix + "mDevicePolicies=" + mDevicePolicies);
pw.println(prefix + "mVirtualSensorConfigs=" + mVirtualSensorConfigs);
+ pw.println(prefix + "mHomeComponent=" + mHomeComponent);
+ pw.println(prefix + "mInputMethodComponent=" + mInputMethodComponent);
pw.println(prefix + "mAudioPlaybackSessionId=" + mAudioPlaybackSessionId);
pw.println(prefix + "mAudioRecordingSessionId=" + mAudioRecordingSessionId);
}
@@ -644,16 +667,17 @@
private int mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED;
private boolean mDefaultActivityPolicyConfigured = false;
@Nullable private String mName;
- @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
+ @NonNull private final SparseIntArray mDevicePolicies = new SparseIntArray();
private int mAudioPlaybackSessionId = AUDIO_SESSION_ID_GENERATE;
private int mAudioRecordingSessionId = AUDIO_SESSION_ID_GENERATE;
- @NonNull private List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
+ @NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs = new ArrayList<>();
@Nullable private Executor mVirtualSensorCallbackExecutor;
@Nullable private VirtualSensorCallback mVirtualSensorCallback;
@Nullable private Executor mVirtualSensorDirectChannelCallbackExecutor;
@Nullable private VirtualSensorDirectChannelCallback mVirtualSensorDirectChannelCallback;
@Nullable private ComponentName mHomeComponent;
+ @Nullable private ComponentName mInputMethodComponent;
private static class VirtualSensorCallbackDelegate extends IVirtualSensorCallback.Stub {
@NonNull
@@ -749,6 +773,28 @@
}
/**
+ * Specifies a component to be used as input method on all displays owned by this virtual
+ * device.
+ *
+ * @param inputMethodComponent The component name to be used as input method. Must comply to
+ * all general input method requirements described in the guide to
+ * <a href="{@docRoot}guide/topics/text/creating-input-method.html">
+ * Creating an Input Method</a>. If the given component is not available for any user that
+ * may interact with the virtual device, then there will effectively be no IME on this
+ * device's displays for that user.
+ *
+ * @see android.inputmethodservice.InputMethodService
+ * @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly
+ * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker
+ */
+ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_IME)
+ @NonNull
+ public Builder setInputMethodComponent(@Nullable ComponentName inputMethodComponent) {
+ mInputMethodComponent = inputMethodComponent;
+ return this;
+ }
+
+ /**
* Sets the user handles with matching managed accounts on the remote device to which
* this virtual device is streaming. The caller is responsible for verifying the presence
* and legitimacy of a matching managed account on the remote device.
@@ -1136,6 +1182,7 @@
mName,
mDevicePolicies,
mHomeComponent,
+ mInputMethodComponent,
mVirtualSensorConfigs,
virtualSensorCallbackDelegate,
mAudioPlaybackSessionId,
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 034a55a..e1c1a42 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3855,8 +3855,9 @@
device -->
<attr name="isVrOnly" format="boolean"/>
<!-- Specifies if an IME can only be used on a display created by a virtual device.
- @hide @SystemApi
- @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") -->
+ @see android.companion.virtual.VirtualDeviceParams.Builder#setInputMethodComponent
+ @hide @SystemApi -->
+ <!-- @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") -->
<attr name="isVirtualDeviceOnly" format="boolean"/>
<attr name="__removed2" format="boolean" />
<!-- Specifies whether the IME supports showing inline suggestions. -->
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 45d7314..13c7924 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -104,6 +104,7 @@
import com.android.server.companion.virtual.GenericWindowPolicyController.RunningAppsChangedListener;
import com.android.server.companion.virtual.audio.VirtualAudioController;
import com.android.server.companion.virtual.camera.VirtualCameraController;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -352,6 +353,14 @@
flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
}
mBaseVirtualDisplayFlags = flags;
+
+ if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) {
+ final String imeId = mParams.getInputMethodComponent().flattenToShortString();
+ Slog.d(TAG, "Setting custom input method " + imeId + " as default for virtual device "
+ + deviceId);
+ InputMethodManagerInternal.get().setVirtualDeviceInputMethodForAllUsers(
+ mDeviceId, imeId);
+ }
}
@VisibleForTesting
@@ -556,6 +565,12 @@
mCameraAccessController.stopObservingIfNeeded();
}
+ // Clear any previously set custom IME components.
+ if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) {
+ InputMethodManagerInternal.get().setVirtualDeviceInputMethodForAllUsers(
+ mDeviceId, null);
+ }
+
mInputController.close();
mSensorController.close();
} finally {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index ffd714b..f526dbe 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -120,6 +120,25 @@
@UserIdInt int userId);
/**
+ * Makes the input method associated with {@code imeId} the default input method for all users
+ * on displays that are owned by the virtual device with the given {@code deviceId}. If the
+ * input method associated with {@code imeId} is not available, there will be no IME on the
+ * relevant displays.
+ *
+ * <p>The caller of this method is responsible for resetting it to {@code null} after the
+ * virtual device is closed.</p>
+ *
+ * @param deviceId the device ID on which to use the given input method as default.
+ * @param imeId the input method ID to be used as default on the given device. If {@code null},
+ * then any existing input method association with that device will be removed.
+ * @throws IllegalArgumentException if a non-{@code null} input method ID is passed for a
+ * device ID that already has a custom input method set or if
+ * the device ID is not a valid virtual device.
+ */
+ public abstract void setVirtualDeviceInputMethodForAllUsers(
+ int deviceId, @Nullable String imeId);
+
+ /**
* Registers a new {@link InputMethodListListener}.
*
* @param listener the listener to add
@@ -250,6 +269,11 @@
}
@Override
+ public void setVirtualDeviceInputMethodForAllUsers(
+ int deviceId, @Nullable String imeId) {
+ }
+
+ @Override
public void registerInputMethodListListener(InputMethodListListener listener) {
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 85c57de..09c388f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -169,6 +169,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
import com.android.internal.view.IInputMethodManager;
import com.android.server.AccessibilityManagerInternal;
import com.android.server.EventLogTags;
@@ -312,6 +313,9 @@
// All known input methods.
final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
+ // Mapping from deviceId to the device-specific imeId for that device.
+ private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>();
+
final InputMethodSubtypeSwitchingController mSwitchingController;
final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController =
new HardwareKeyboardShortcutController();
@@ -5620,6 +5624,23 @@
}
@Override
+ public void setVirtualDeviceInputMethodForAllUsers(int deviceId, @Nullable String imeId) {
+ // TODO(b/287269288): validate that id belongs to a valid virtual device instead.
+ Preconditions.checkArgument(deviceId == Context.DEVICE_ID_DEFAULT,
+ "DeviceId " + deviceId + " does not belong to a virtual device.");
+ synchronized (ImfLock.class) {
+ if (imeId == null) {
+ mVirtualDeviceMethodMap.remove(deviceId);
+ } else if (mVirtualDeviceMethodMap.contains(deviceId)) {
+ throw new IllegalArgumentException("Virtual device " + deviceId
+ + " already has a custom input method component");
+ } else {
+ mVirtualDeviceMethodMap.put(deviceId, imeId);
+ }
+ }
+ }
+
+ @Override
public void registerInputMethodListListener(InputMethodListListener listener) {
mInputMethodListListeners.addIfAbsent(listener);
}