Loading services/core/java/com/android/server/input/KeyboardLayoutManager.java +39 −11 Original line number Original line Diff line number Diff line Loading @@ -76,6 +76,8 @@ import com.android.internal.inputmethod.InputMethodSubtypeHandle; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.XmlUtils; import com.android.internal.util.XmlUtils; import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria; import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; Loading Loading @@ -197,7 +199,7 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); if (config == null) { if (config == null) { config = new KeyboardConfiguration(); config = new KeyboardConfiguration(deviceId); mConfiguredKeyboards.put(deviceId, config); mConfiguredKeyboards.put(deviceId, config); } } Loading Loading @@ -1093,19 +1095,26 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { @MainThread @MainThread private void maybeUpdateNotification() { private void maybeUpdateNotification() { if (mConfiguredKeyboards.size() == 0) { List<KeyboardConfiguration> configurations = new ArrayList<>(); hideKeyboardLayoutNotification(); return; } for (int i = 0; i < mConfiguredKeyboards.size(); i++) { for (int i = 0; i < mConfiguredKeyboards.size(); i++) { int deviceId = mConfiguredKeyboards.keyAt(i); KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); if (isVirtualDevice(deviceId)) { continue; } // If we have a keyboard with no selected layouts, we should always show missing // If we have a keyboard with no selected layouts, we should always show missing // layout notification even if there are other keyboards that are configured properly. // layout notification even if there are other keyboards that are configured properly. if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) { if (!config.hasConfiguredLayouts()) { showMissingKeyboardLayoutNotification(); showMissingKeyboardLayoutNotification(); return; return; } } configurations.add(config); } } showConfiguredKeyboardLayoutNotification(); if (configurations.size() == 0) { hideKeyboardLayoutNotification(); return; } showConfiguredKeyboardLayoutNotification(configurations); } } @MainThread @MainThread Loading Loading @@ -1185,10 +1194,11 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } @MainThread @MainThread private void showConfiguredKeyboardLayoutNotification() { private void showConfiguredKeyboardLayoutNotification( List<KeyboardConfiguration> configurations) { final Resources r = mContext.getResources(); final Resources r = mContext.getResources(); if (mConfiguredKeyboards.size() != 1) { if (configurations.size() != 1) { showKeyboardLayoutNotification( showKeyboardLayoutNotification( r.getString(R.string.keyboard_layout_notification_multiple_selected_title), r.getString(R.string.keyboard_layout_notification_multiple_selected_title), r.getString(R.string.keyboard_layout_notification_multiple_selected_message), r.getString(R.string.keyboard_layout_notification_multiple_selected_message), Loading @@ -1196,8 +1206,8 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return; return; } } final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0)); final KeyboardConfiguration config = configurations.get(0); final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0); final InputDevice inputDevice = getInputDevice(config.getDeviceId()); if (inputDevice == null || !config.hasConfiguredLayouts()) { if (inputDevice == null || !config.hasConfiguredLayouts()) { return; return; } } Loading Loading @@ -1356,6 +1366,13 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return false; return false; } } @VisibleForTesting public boolean isVirtualDevice(int deviceId) { VirtualDeviceManagerInternal vdm = LocalServices.getService( VirtualDeviceManagerInternal.class); return vdm == null || vdm.isInputDeviceOwnedByVirtualDevice(deviceId); } private static int[] getScriptCodes(@Nullable Locale locale) { private static int[] getScriptCodes(@Nullable Locale locale) { if (locale == null) { if (locale == null) { return new int[0]; return new int[0]; Loading Loading @@ -1430,11 +1447,22 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } private static class KeyboardConfiguration { private static class KeyboardConfiguration { // If null or empty, it means no layout is configured for the device. And user needs to // If null or empty, it means no layout is configured for the device. And user needs to // manually set up the device. // manually set up the device. @Nullable @Nullable private Set<String> mConfiguredLayouts; private Set<String> mConfiguredLayouts; private final int mDeviceId; private KeyboardConfiguration(int deviceId) { mDeviceId = deviceId; } private int getDeviceId() { return mDeviceId; } private boolean hasConfiguredLayouts() { private boolean hasConfiguredLayouts() { return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); } } Loading tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +47 −0 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input package com.android.server.input import android.app.NotificationManager import android.content.Context import android.content.Context import android.content.ContextWrapper import android.content.ContextWrapper import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo Loading Loading @@ -129,6 +130,8 @@ class KeyboardLayoutManagerTests { @Mock @Mock private lateinit var packageManager: PackageManager private lateinit var packageManager: PackageManager @Mock private lateinit var notificationManager: NotificationManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var imeInfo: InputMethodInfo private lateinit var imeInfo: InputMethodInfo Loading Loading @@ -163,6 +166,8 @@ class KeyboardLayoutManagerTests { keyboardLayoutManager = Mockito.spy( keyboardLayoutManager = Mockito.spy( KeyboardLayoutManager(context, native, dataStore, testLooper.looper) KeyboardLayoutManager(context, native, dataStore, testLooper.looper) ) ) Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE))) .thenReturn(notificationManager) setupInputDevices() setupInputDevices() setupBroadcastReceiver() setupBroadcastReceiver() setupIme() setupIme() Loading Loading @@ -946,6 +951,48 @@ class KeyboardLayoutManagerTests { } } } } @Test fun testNotificationShown_onInputDeviceChanged() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) NewSettingsApiFlag(true).use { keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) ExtendedMockito.verify( notificationManager, Mockito.times(1) ).notifyAsUser( ArgumentMatchers.isNull(), ArgumentMatchers.anyInt(), ArgumentMatchers.any(), ArgumentMatchers.any() ) } } @Test fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) NewSettingsApiFlag(true).use { keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) ExtendedMockito.verify( notificationManager, Mockito.never() ).notifyAsUser( ArgumentMatchers.isNull(), ArgumentMatchers.anyInt(), ArgumentMatchers.any(), ArgumentMatchers.any() ) } } private fun assertCorrectLayout( private fun assertCorrectLayout( device: InputDevice, device: InputDevice, imeSubtype: InputMethodSubtype, imeSubtype: InputMethodSubtype, Loading Loading
services/core/java/com/android/server/input/KeyboardLayoutManager.java +39 −11 Original line number Original line Diff line number Diff line Loading @@ -76,6 +76,8 @@ import com.android.internal.inputmethod.InputMethodSubtypeHandle; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.XmlUtils; import com.android.internal.util.XmlUtils; import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria; import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; Loading Loading @@ -197,7 +199,7 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); if (config == null) { if (config == null) { config = new KeyboardConfiguration(); config = new KeyboardConfiguration(deviceId); mConfiguredKeyboards.put(deviceId, config); mConfiguredKeyboards.put(deviceId, config); } } Loading Loading @@ -1093,19 +1095,26 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { @MainThread @MainThread private void maybeUpdateNotification() { private void maybeUpdateNotification() { if (mConfiguredKeyboards.size() == 0) { List<KeyboardConfiguration> configurations = new ArrayList<>(); hideKeyboardLayoutNotification(); return; } for (int i = 0; i < mConfiguredKeyboards.size(); i++) { for (int i = 0; i < mConfiguredKeyboards.size(); i++) { int deviceId = mConfiguredKeyboards.keyAt(i); KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); if (isVirtualDevice(deviceId)) { continue; } // If we have a keyboard with no selected layouts, we should always show missing // If we have a keyboard with no selected layouts, we should always show missing // layout notification even if there are other keyboards that are configured properly. // layout notification even if there are other keyboards that are configured properly. if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) { if (!config.hasConfiguredLayouts()) { showMissingKeyboardLayoutNotification(); showMissingKeyboardLayoutNotification(); return; return; } } configurations.add(config); } } showConfiguredKeyboardLayoutNotification(); if (configurations.size() == 0) { hideKeyboardLayoutNotification(); return; } showConfiguredKeyboardLayoutNotification(configurations); } } @MainThread @MainThread Loading Loading @@ -1185,10 +1194,11 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } @MainThread @MainThread private void showConfiguredKeyboardLayoutNotification() { private void showConfiguredKeyboardLayoutNotification( List<KeyboardConfiguration> configurations) { final Resources r = mContext.getResources(); final Resources r = mContext.getResources(); if (mConfiguredKeyboards.size() != 1) { if (configurations.size() != 1) { showKeyboardLayoutNotification( showKeyboardLayoutNotification( r.getString(R.string.keyboard_layout_notification_multiple_selected_title), r.getString(R.string.keyboard_layout_notification_multiple_selected_title), r.getString(R.string.keyboard_layout_notification_multiple_selected_message), r.getString(R.string.keyboard_layout_notification_multiple_selected_message), Loading @@ -1196,8 +1206,8 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return; return; } } final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0)); final KeyboardConfiguration config = configurations.get(0); final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0); final InputDevice inputDevice = getInputDevice(config.getDeviceId()); if (inputDevice == null || !config.hasConfiguredLayouts()) { if (inputDevice == null || !config.hasConfiguredLayouts()) { return; return; } } Loading Loading @@ -1356,6 +1366,13 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return false; return false; } } @VisibleForTesting public boolean isVirtualDevice(int deviceId) { VirtualDeviceManagerInternal vdm = LocalServices.getService( VirtualDeviceManagerInternal.class); return vdm == null || vdm.isInputDeviceOwnedByVirtualDevice(deviceId); } private static int[] getScriptCodes(@Nullable Locale locale) { private static int[] getScriptCodes(@Nullable Locale locale) { if (locale == null) { if (locale == null) { return new int[0]; return new int[0]; Loading Loading @@ -1430,11 +1447,22 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } private static class KeyboardConfiguration { private static class KeyboardConfiguration { // If null or empty, it means no layout is configured for the device. And user needs to // If null or empty, it means no layout is configured for the device. And user needs to // manually set up the device. // manually set up the device. @Nullable @Nullable private Set<String> mConfiguredLayouts; private Set<String> mConfiguredLayouts; private final int mDeviceId; private KeyboardConfiguration(int deviceId) { mDeviceId = deviceId; } private int getDeviceId() { return mDeviceId; } private boolean hasConfiguredLayouts() { private boolean hasConfiguredLayouts() { return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); } } Loading
tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +47 −0 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input package com.android.server.input import android.app.NotificationManager import android.content.Context import android.content.Context import android.content.ContextWrapper import android.content.ContextWrapper import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo Loading Loading @@ -129,6 +130,8 @@ class KeyboardLayoutManagerTests { @Mock @Mock private lateinit var packageManager: PackageManager private lateinit var packageManager: PackageManager @Mock private lateinit var notificationManager: NotificationManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var imeInfo: InputMethodInfo private lateinit var imeInfo: InputMethodInfo Loading Loading @@ -163,6 +166,8 @@ class KeyboardLayoutManagerTests { keyboardLayoutManager = Mockito.spy( keyboardLayoutManager = Mockito.spy( KeyboardLayoutManager(context, native, dataStore, testLooper.looper) KeyboardLayoutManager(context, native, dataStore, testLooper.looper) ) ) Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE))) .thenReturn(notificationManager) setupInputDevices() setupInputDevices() setupBroadcastReceiver() setupBroadcastReceiver() setupIme() setupIme() Loading Loading @@ -946,6 +951,48 @@ class KeyboardLayoutManagerTests { } } } } @Test fun testNotificationShown_onInputDeviceChanged() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) NewSettingsApiFlag(true).use { keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) ExtendedMockito.verify( notificationManager, Mockito.times(1) ).notifyAsUser( ArgumentMatchers.isNull(), ArgumentMatchers.anyInt(), ArgumentMatchers.any(), ArgumentMatchers.any() ) } } @Test fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) NewSettingsApiFlag(true).use { keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) ExtendedMockito.verify( notificationManager, Mockito.never() ).notifyAsUser( ArgumentMatchers.isNull(), ArgumentMatchers.anyInt(), ArgumentMatchers.any(), ArgumentMatchers.any() ) } } private fun assertCorrectLayout( private fun assertCorrectLayout( device: InputDevice, device: InputDevice, imeSubtype: InputMethodSubtype, imeSubtype: InputMethodSubtype, Loading