[Mag] Keyboard shortcut info shown when physical keyboard present

Shows keyboard shortcut info in magnification settings when a
physical keyboard is present. See bug for screenshots.

Hides touchscreen info in magnification settings when a touchscreen
is not present.

Adds ShadowInputDevice support for physical full keyboards.

Bug: b/388847050
Test: Manual, atest ToggleScreenMagnificationPreferenceFragmentTest
Flag: com.android.server.accessibility.enable_magnification_keyboard_control
Change-Id: Ib53fbd8f929d1cc8e294f6f04bab405c9bb576a9
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c22ce6c..f8937f7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -5330,6 +5330,16 @@
         {4,number,integer}. Lift finger to stop magnification
         ]]>
     </string>
+    <!-- Instructions on the accessibility preference screen teaching the user how to control magnification with a keyboard. [CHAR LIMIT=none] -->
+    <string name="accessibility_screen_magnification_keyboard_summary">
+        <![CDATA[
+        <b>To zoom with the keyboard:</b><br/>
+        {0,number,integer}. Use the shortcut to start magnification<br/>
+        {1,number,integer}. Hold down <xliff:g id="meta1">%1$s</xliff:g> and <xliff:g id="alt1">%2$s</xliff:g> and press + or - to zoom in or out<br/>
+        {2,number,integer}. Hold down <xliff:g id="meta2">%3$s</xliff:g> and <xliff:g id="alt2">%4$s</xliff:g> and press the arrow keys to move around the screen<br/>
+        {3,number,integer}. Use the shortcut to stop magnification
+        ]]>
+    </string>
     <!-- Instructions on the accessibility preference screen teaching the user how to interact with screen magnification when one finger panning feature is turned off. [CHAR LIMIT=none] -->
     <string name="accessibility_screen_magnification_summary_one_finger_panning_off">
         <![CDATA[
diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
index c9b8b2b..8b52507 100644
--- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java
@@ -35,6 +35,7 @@
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.view.InputDevice;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -231,9 +232,32 @@
 
         if (!arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)
                 && !Flags.enableMagnificationOneFingerPanningGesture()) {
-            String summary = MessageFormat.format(
-                    context.getString(R.string.accessibility_screen_magnification_summary),
-                    new Object[]{1, 2, 3, 4, 5});
+            String summary = "";
+            boolean hasTouchscreen = hasTouchscreen();
+            if (Flags.enableMagnificationKeyboardControl() && hasHardKeyboard()) {
+                // Include the keyboard summary when a keyboard is plugged in.
+                final String meta = context.getString(R.string.modifier_keys_meta);
+                final String alt = context.getString(R.string.modifier_keys_alt);
+                summary += MessageFormat.format(
+                        context.getString(
+                                R.string.accessibility_screen_magnification_keyboard_summary,
+                                meta, alt, meta, alt),
+                        new Object[]{1, 2, 3, 4});
+                if (hasTouchscreen) {
+                    // Add a newline before the touchscreen text.
+                    summary += "<br/><br/>";
+                }
+
+            }
+            if (hasTouchscreen || TextUtils.isEmpty(summary)) {
+                // Always show the touchscreen summary if there is no summary yet, even if the
+                // touchscreen is missing.
+                // If the keyboard summary is present and there is no touchscreen, then we can
+                // ignore the touchscreen summary.
+                summary += MessageFormat.format(
+                        context.getString(R.string.accessibility_screen_magnification_summary),
+                        new Object[]{1, 2, 3, 4, 5});
+            }
             arguments.putCharSequence(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, summary);
         }
 
@@ -610,6 +634,25 @@
                 getPrefContext(), MAGNIFICATION_CONTROLLER_NAME);
     }
 
+    private boolean hasHardKeyboard() {
+        final int[] devices = InputDevice.getDeviceIds();
+        for (int i = 0; i < devices.length; i++) {
+            InputDevice device = InputDevice.getDevice(devices[i]);
+            if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
+                continue;
+            }
+
+            return true;
+        }
+        return false;
+    }
+
+    private boolean hasTouchscreen() {
+        return getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
+                || getPackageManager().hasSystemFeature(PackageManager.FEATURE_FAKETOUCH);
+    }
+
     public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
             new BaseSearchIndexProvider() {
                 // LINT.IfChange(search_data)
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
index 4b28085..3c136f0 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java
@@ -51,6 +51,7 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.view.InputDevice;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -68,6 +69,7 @@
 import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
 import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
 import com.android.settings.testutils.shadow.ShadowDeviceConfig;
+import com.android.settings.testutils.shadow.ShadowInputDevice;
 import com.android.settings.testutils.shadow.ShadowStorageManager;
 import com.android.settings.testutils.shadow.ShadowUserManager;
 import com.android.settingslib.core.lifecycle.LifecycleObserver;
@@ -169,6 +171,7 @@
     @After
     public void tearDown() {
         ShadowDeviceConfig.reset();
+        ShadowInputDevice.reset();
     }
 
     @Test
@@ -672,6 +675,36 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_KEYBOARD_CONTROL)
+    public void getCurrentHtmlDescription_doesNotIncludeKeyboardInfoIfNoKeyboardAttached() {
+        ToggleScreenMagnificationPreferenceFragment fragment =
+                mFragController.create(
+                        R.id.main_content, /* bundle= */ null).start().resume().get();
+
+        String htmlDescription = fragment.getCurrentHtmlDescription().toString();
+        assertThat(htmlDescription).isNotEmpty();
+        assertThat(htmlDescription).doesNotContain("keyboard");
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_KEYBOARD_CONTROL)
+    @Config(shadows = ShadowInputDevice.class)
+    public void getCurrentHtmlDescription_includesKeyboardInfoIfKeyboardAttached() {
+        int deviceId = 1;
+        ShadowInputDevice.sDeviceIds = new int[]{deviceId};
+        InputDevice device = ShadowInputDevice.makeFullKeyboardInputDevicebyId(deviceId);
+        ShadowInputDevice.addDevice(deviceId, device);
+
+        ToggleScreenMagnificationPreferenceFragment fragment =
+                mFragController.create(
+                        R.id.main_content, /* bundle= */ null).start().resume().get();
+
+        String htmlDescription = fragment.getCurrentHtmlDescription().toString();
+        assertThat(htmlDescription).isNotEmpty();
+        assertThat(htmlDescription).contains("keyboard");
+    }
+
+    @Test
     public void getSummary_magnificationEnabled_returnShortcutOnWithSummary() {
         mShadowAccessibilityManager.setAccessibilityShortcutTargets(
                 TRIPLETAP, List.of(MAGNIFICATION_CONTROLLER_NAME));
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputDevice.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputDevice.java
index 145c2e9..a448e17 100644
--- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputDevice.java
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputDevice.java
@@ -37,6 +37,8 @@
 
     private int mSources;
 
+    private boolean mIsFullKeyboard;
+
     @Implementation
     protected static int[] getDeviceIds() {
         return sDeviceIds;
@@ -62,6 +64,28 @@
         return mDeviceId;
     }
 
+    public void setId(int id) {
+        mDeviceId = id;
+    }
+
+    @Implementation
+    public int getSources() {
+        return mSources;
+    }
+
+    public void setSources(int sources) {
+        mSources = sources;
+    }
+
+    @Implementation
+    public boolean isFullKeyboard() {
+        return mIsFullKeyboard;
+    }
+
+    public void setFullKeyboard(boolean isFullKeyboard) {
+        mIsFullKeyboard = isFullKeyboard;
+    }
+
     public static InputDevice makeInputDevicebyId(int id) {
         final InputDevice inputDevice = Shadow.newInstanceOf(InputDevice.class);
         final ShadowInputDevice shadowInputDevice = Shadow.extract(inputDevice);
@@ -69,10 +93,6 @@
         return inputDevice;
     }
 
-    public void setId(int id) {
-        mDeviceId = id;
-    }
-
     public static InputDevice makeInputDevicebyIdWithSources(int id, int sources) {
         final InputDevice inputDevice = Shadow.newInstanceOf(InputDevice.class);
         final ShadowInputDevice shadowInputDevice = Shadow.extract(inputDevice);
@@ -81,12 +101,17 @@
         return inputDevice;
     }
 
-    @Implementation
-    public int getSources() {
-        return mSources;
-    }
-
-    public void setSources(int sources) {
-        mSources = sources;
+    /**
+     * Create a full keyboard input device shadow.
+     * @param id The ID to use. If the ID is < 1, the device is considered virtual.
+     * @return The shadow InputDevice
+     */
+    public static InputDevice makeFullKeyboardInputDevicebyId(int id) {
+        final InputDevice inputDevice = Shadow.newInstanceOf(InputDevice.class);
+        final ShadowInputDevice shadowInputDevice = Shadow.extract(inputDevice);
+        shadowInputDevice.setId(id);
+        shadowInputDevice.setFullKeyboard(true);
+        shadowInputDevice.setSources(InputDevice.SOURCE_KEYBOARD);
+        return inputDevice;
     }
 }