Initial porting of View and Res for AccessibilityMenu

Bug: 261252772
Test: adb install the APK, then enable the menu from the Settings Accessibility page
Change-Id: I20b11bcaa631a5c78f00e870bef5d25d02fe86dc
This commit is contained in:
Riley Jones
2022-12-08 21:36:39 +00:00
parent 1dcfaa3ac7
commit adf5e308c3
37 changed files with 1684 additions and 3 deletions

View File

@@ -20,6 +20,17 @@ package {
android_app {
name: "AccessibilityMenu",
static_libs: [
"androidx.coordinatorlayout_coordinatorlayout",
"androidx.core_core",
"androidx.viewpager_viewpager",
],
uses_libs: [
"org.apache.http.legacy",
],
srcs: [
"src/**/*.java",
],

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false"
android:color="@color/footer_icon_disabled_color" /> <!-- disabled -->
<item android:color="@color/footer_icon_enabled_color" /> <!-- default -->
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<ripple
android:color="@color/ripple_material_color">
<item android:id="@android:id/mask">
<color android:color="@color/overlay_bg_color"/>
</item>
</ripple>
</item>
</layer-list>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<ripple
android:color="@color/ripple_material_color">
<item android:id="@android:id/mask">
<color android:color="@color/overlay_bg_color"/>
</item>
</ripple>
</item>
</layer-list>

View File

@@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector android:height="108dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M7.875,10.625C7.1188,10.625 6.5,11.2437 6.5,12C6.5,12.7562 7.1188,13.375 7.875,13.375C8.6313,13.375 9.25,12.7562 9.25,12C9.25,11.2437 8.6313,10.625 7.875,10.625ZM16.125,10.625C15.3687,10.625 14.75,11.2437 14.75,12C14.75,12.7562 15.3687,13.375 16.125,13.375C16.8813,13.375 17.5,12.7562 17.5,12C17.5,11.2437 16.8813,10.625 16.125,10.625ZM10.625,12C10.625,11.2437 11.2438,10.625 12,10.625C12.7563,10.625 13.375,11.2437 13.375,12C13.375,12.7562 12.7563,13.375 12,13.375C11.2438,13.375 10.625,12.7562 10.625,12Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="32dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/footer_arrow_length"
android:height="@dimen/footer_arrow_length"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/footer_icon_color"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="@dimen/footer_arrow_length"
android:height="@dimen/footer_arrow_length"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/footer_icon_color"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

View File

@@ -0,0 +1,33 @@
<vector android:height="48dp" android:viewportHeight="192.0"
android:viewportWidth="192.0" android:width="48dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#34a853" android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
<path android:pathData="M137.61,94.07l-17,17 -17,-17 -17,17L70.3,94.72l-17,17L125.66,184h15.56a14.32,14.32 0,0 0,13.67 -10.19l15.25,-47.21Z">
<aapt:attr name="android:fillColor">
<gradient android:endX="27152.64"
android:endY="32745.600000000002"
android:startX="20910.72"
android:startY="21934.079999999998" android:type="linear">
<item android:color="#33263238" android:offset="0.0"/>
<item android:color="#11205432" android:offset="0.47"/>
<item android:color="#051E6130" android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M50.14,100.11a12,12 0,1 1,11.39 15.77,11.72 11.72,0 0,1 -5,-1.1l-3.41,-3.4ZM129.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.4 91.88ZM95.4,91.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 91.88Z"/>
<path android:fillColor="#fff"
android:pathData="M61.53,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,61.53 90.88ZM129.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,129.41 90.88ZM95.41,90.88a12,12 0,1 1,-12 12A12,12 0,0 1,95.42 90.88Z"
android:strokeAlpha="0" android:strokeColor="#000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.5"/>
<path android:fillAlpha="0.2" android:fillColor="#263238" android:pathData="M184,80.91a14.33,14.33 0,0 1,-0.63 4.7l-28.48,88.18A14.33,14.33 0,0 1,141.21 184H50.84a14.33,14.33 0,0 1,-13.7 -10.26l-28.53,-90A14.49,14.49 0,0 1,8 79.11a14.3,14.3 0,0 0,0.61 3.64l28.53,90A14.33,14.33 0,0 0,50.84 183h90.37a14.33,14.33 0,0 0,13.67 -10.19l28.48,-88.18A14.79,14.79 0,0 0,184 80.91Z"/>
<path android:fillAlpha="0.2" android:fillColor="#fff" android:pathData="M184,81.89A14.46,14.46 0,0 0,178.57 71L104.76,12.1A14.21,14.21 0,0 0,87.15 12L13.58,69.12A14.5,14.5 0,0 0,8 80.09a14.5,14.5 0,0 1,5.57 -12L87.15,11a14.21,14.21 0,0 1,17.61 0.12L178.57,70A14.48,14.48 0,0 1,184 81.89Z"/>
<path android:pathData="M37.14,173.74l-28.53,-90a14.53,14.53 0,0 1,5 -15.63L87.15,11a14.21,14.21 0,0 1,17.61 0.12l73.81,58.94a14.53,14.53 0,0 1,4.8 15.57l-28.48,88.18A14.32,14.32 0,0 1,141.22 184H50.84A14.33,14.33 0,0 1,37.14 173.74Z"/>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorAccessibilityMenuIcon" />
<foreground>
<inset
android:drawable="@drawable/ic_a11y_menu_round"
android:inset="21.88%" />
</foreground>
</adaptive-icon>
</vector>

View File

@@ -0,0 +1,8 @@
<vector android:height="32dp"
android:viewportHeight="192.0" android:viewportWidth="192.0"
android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#34A853" android:pathData="M172,60m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"/>
<path android:fillColor="#EA4335" android:pathData="M136,88m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
<path android:fillColor="#FBBC05" android:pathData="M136,148m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"/>
<path android:fillColor="#4285F4" android:pathData="M56,64m-48,0a48,48 0,1 1,96 0a48,48 0,1 1,-96 0"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,5 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/overlay_bg_color" />
</shape>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/footerlayout"
android:layout_width="match_parent"
android:layout_height="@dimen/grid_item_btn_view_height"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical"
android:visibility="gone">
<View
android:id="@+id/top_listDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal">
<ImageButton
android:id="@+id/menu_prev_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/footer_button_background_left"
android:contentDescription="@string/previous_button_content_description"
android:scaleType="centerInside"
android:src="@drawable/ic_arrow_back_24dp"
android:tint="@color/footer_icon_tint_color"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?android:attr/listDivider"/>
<ImageButton
android:id="@+id/menu_next_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/footer_button_background_right"
android:contentDescription="@string/next_button_content_description"
android:scaleType="centerInside"
android:src="@drawable/ic_arrow_forward_24dp"
android:tint="@color/footer_icon_tint_color"/>
</LinearLayout>
<View
android:id="@+id/bottom_listDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>
</LinearLayout>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/grid_item_padding"
android:paddingBottom="@dimen/grid_item_padding"
android:gravity="center">
<ImageButton
android:id="@+id/shortcutIconBtn"
android:layout_width="@dimen/image_button_width"
android:layout_height="@dimen/image_button_height"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:scaleType="fitCenter"></ImageButton>
<TextView
android:id="@+id/shortcutLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/grid_item_text_view_margin_top"
android:layout_below="@+id/shortcutIconBtn"
android:layout_centerHorizontal="true"
android:ellipsize="end"
android:gravity="center_horizontal"
android:importantForAccessibility="no"
android:lines="2"
android:textSize="@dimen/label_text_size"
android:textAlignment="center"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Widget.Button"/>
</RelativeLayout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/gridview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:horizontalSpacing="@dimen/a11ymenu_grid_layout_margin"
android:listSelector="@android:color/transparent"
android:numColumns="3"
android:overScrollMode="never"
android:stretchMode="columnWidth">
</GridView>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/row_width"
android:layout_height="match_parent"
android:id="@+id/coordinatorLayout"
android:background="@drawable/view_background"
>
<LinearLayout
android:layout_width="@dimen/row_width"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/table_margin_top"
android:paddingBottom="@dimen/a11ymenu_layout_margin"
android:paddingLeft="@dimen/a11ymenu_layout_margin"
android:paddingRight="@dimen/a11ymenu_layout_margin"
android:layout_gravity="center"
android:gravity="center"
/>
<include layout="@layout/footerlayout_switch_page"/>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="table_margin_top">0dp</dimen>
<dimen name="row_width">388dp</dimen>
<dimen name="image_button_height">45dp</dimen>
<dimen name="image_button_width">45dp</dimen>
<dimen name="image_button_marginBottom">1dp</dimen>
<!-- dimens for gridview layout. -->
<dimen name="grid_item_padding">4dp</dimen>
</resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="power_color">#dadce0</color>
<color name="quick_settings_color">#78d9ec</color>
<color name="a11y_settings_color">#d9affe</color>
<color name="recent_apps_color">#f0a5dd</color>
<color name="lockscreen_color">#85e4a0</color>
<color name="volume_color">#7ae3d4</color>
<color name="notifications_color">#f496ac</color>
<color name="screenshot_color">#adcbff</color>
<color name="assistant_color">#F1F3F4</color>
<color name="brightness_color">#fdd663</color>
<color name="ripple_material_color">#10FFFFFF</color>
<color name="overlay_bg_color">#313235</color>
<color name="footer_icon_color">#E8EAED</color>
<color name="footer_icon_enabled_color">#E8EAED</color>
<color name="footer_icon_disabled_color">#5F6368</color>
<color name="colorControlNormal">#202124</color>
</resources>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!--Adds the theme to support SnackBar component and user configurable theme. -->
<style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight">
<item name="android:colorControlNormal">@color/colorControlNormal</item>
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="isAtLeastP">true</bool>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="power_color">#757575</color>
<color name="quick_settings_color">#2196F3</color>
<color name="a11y_settings_color">#5806C9</color>
<color name="recent_apps_color">#AD2EC6</color>
<color name="lockscreen_color">#0F9D58</color>
<color name="volume_color">#01A2A0</color>
<color name="notifications_color">#F15B8D</color>
<color name="screenshot_color">#26459C</color>
<color name="assistant_color">#F1F3F4</color>
<color name="brightness_color">#E59810</color>
<color name="colorAccent">#1a73e8</color>
<color name="ripple_material_color">#1f000000</color>
<color name="overlay_bg_color">@android:color/white</color>
<color name="footer_icon_color">@android:color/black</color>
<color name="footer_icon_enabled_color">@android:color/black</color>
<color name="footer_icon_disabled_color">#ddd</color>
<color name="colorControlNormal">@android:color/white</color>
<color name="colorAccessibilityMenuIcon">#3AA757</color>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- the curve radius for the background of the complete layout -->
<dimen name="table_margin_top">22dp</dimen>
<dimen name="row_width">@dimen/custom_match_parent</dimen>
<dimen name="image_button_height">60dp</dimen>
<dimen name="image_button_width">60dp</dimen>
<dimen name="image_button_marginBottom">2dp</dimen>
<dimen name="a11ymenu_layout_margin">4dp</dimen>
<dimen name="custom_match_parent">-1px</dimen>
<!-- dimens for gridview layout. -->
<dimen name="grid_item_text_view_margin_top">2dp</dimen>
<dimen name="grid_item_padding">10dp</dimen>
<dimen name="grid_item_btn_view_height">48dp</dimen>
<dimen name="a11ymenu_grid_layout_margin">8dp</dimen>
<!-- dimens for a11y menu footer layout. -->
<dimen name="footer_arrow_length">24dp</dimen>
<!-- text size for shortcut label when large button settings in on. -->
<dimen name="large_label_text_size">18sp</dimen>
<dimen name="label_text_size">14sp</dimen>
</resources>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- user customized shortcuts preference -->
<string name="pref_user_shortcuts">accessibility_menu_user_shortcuts</string>
<!-- key for user customized shortcuts -->
<string name="pref_user_shortcuts_key">pref_user_shortcuts_key</string>
<!-- value for empty shortcut -->
<string name="pref_user_shortcuts_value_empty">[]</string>
<!-- empty string for shortcut label -->
<string name="empty_content"></string>
<string name="pref_large_buttons">pref_large_buttons</string>
<!-- key for Help&feedback settings [CHAR_LIMIT=NONE] -->
<string name="pref_help">pref_help</string>
</resources>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- String defining the service name -->
<string name="accessibility_menu_service_name">Accessibility Menu</string>
<!-- Accessibility Menu detail intro. [CHAR_LIMIT=NONE] -->
<string name="accessibility_menu_intro">
The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more.
</string>
<!-- String defining the label for the assistant button -->
<string name="assistant_label">Assistant</string>
<!-- String defining utterance for the assistant button for screen readers -->
<string name="assistant_utterance">Google Assistant</string>
<!-- String defining the label for the accessibility settings button -->
<string name="a11y_settings_label">Accessibility Settings</string>
<!-- String defining the label for the volume button -->
<string name="volume_label">Volume</string>
<!-- String defining utterance for the volume button for screen readers -->
<string name="volume_utterance">Volume controls</string>
<!-- String defining the label for the power button -->
<string name="power_label">Power</string>
<!-- String defining utterance for the power button for screen readers -->
<string name="power_utterance">Power options</string>
<!-- String defining the label for the recent apps button -->
<string name="recent_apps_label">Recent apps</string>
<!-- String defining the label for the lockscreen button -->
<string name="lockscreen_label">Lock screen</string>
<!-- String defining the label for the quick settings button -->
<string name="quick_settings_label">Quick Settings</string>
<!-- String defining the label for the notifications button -->
<string name="notifications_label">Notifications</string>
<!-- String defining the label for the screenshot button -->
<string name="screenshot_label">Screenshot</string>
<!-- String defining the utterance for the screenshot button for screen readers -->
<string name="screenshot_utterance">Take screenshot</string>
<!-- String defining the label for the volume up/down button -->
<string name="volume_up_label">Volume up</string>
<string name="volume_down_label">Volume down</string>
<!-- String defining the label for the brightness up/down button -->
<string name="brightness_up_label">Brightness up</string>
<string name="brightness_down_label">Brightness down</string>
<!-- String defining the content description for the footer previous/next button -->
<string name="previous_button_content_description">Go to previous screen</string>
<string name="next_button_content_description">Go to next screen</string>
<string name="accessibility_menu_description">
The Accessibility Menu provides a large on-screen menu to control your device. You can lock your device, control volume and brightness, take screenshots, and more.
</string>
<!-- Short summary of app that appears as subtext on the service preference in Settings -->
<string name="accessibility_menu_summary">Control device via large menu</string>
<!-- TODO(b/113371047): string need to be reviewed -->
<!-- String defining the settings name -->
<string name="accessibility_menu_settings_name">Accessibility Menu Settings</string>
<!-- String defining the title of Large button setting -->
<string name="accessibility_menu_large_buttons_title">Large buttons</string>
<!-- String defining the summary of Large button setting -->
<string name="accessibility_menu_large_buttons_summary">Increase size of Accessibility Menu Buttons</string>
<!-- String defining the title of the preference to show help and feedback menu [CHAR LIMIT=40] -->
<string name="pref_help_and_feedback_title">Help &#38; feedback</string>
<!-- String defining the title of the preference to show help menu [CHAR LIMIT=40] -->
<string name="pref_help_title">Help</string>
<!-- The percentage of the brightness, and double "%" is required to represent the symbol "%" -->
<string name="brightness_percentage_label">Brightness <xliff:g id="percentage">%1$s</xliff:g> %%</string>
<!-- The percentage of the music volume, and double "%" is required to represent the symbol "%" -->
<string name="music_volume_percentage_label">Music volume <xliff:g id="percentage">%1$s</xliff:g> %%</string>
<!-- The label of a settings item that displays legal information about the licenses used in this app. [CHAR LIMIT=NONE] -->
<string name="pref_item_licenses">Open Source Licenses</string>
</resources>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!--The theme is for preference CollapsingToolbarBaseActivity settings-->
<style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.Light" />
<!--Adds the theme to support SnackBar component and user configurable theme. -->
<style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light">
<item name="android:colorControlNormal">@color/colorControlNormal</item>
</style>
<!--The basic theme for service and test case only-->
<style name="A11yMenuBaseTheme" parent="android:Theme.DeviceDefault.Light">
<item name="android:windowActionBar">false</item>
</style>
</resources>

View File

@@ -13,4 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"/>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRequestAccessibilityButton|flagRequestFilterKeyEvents"
android:canRequestFilterKeyEvents="true"
android:summary="@string/accessibility_menu_summary"
android:intro="@string/accessibility_menu_intro"
android:animatedImageDrawable="@drawable/a11ymenu_intro"
android:isAccessibilityTool="true"
/>

View File

@@ -17,16 +17,39 @@
package com.android.systemui.accessibility.accessibilitymenu;
import android.accessibilityservice.AccessibilityService;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout;
/** @hide */
public class AccessibilityMenuService extends AccessibilityService {
public class AccessibilityMenuService extends AccessibilityService implements View.OnTouchListener {
private static final String TAG = "A11yMenuService";
private A11yMenuOverlayLayout mA11yMenuLayout;
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
public void onCreate() {
super.onCreate();
}
@Override
protected void onServiceConnected() {
mA11yMenuLayout = new A11yMenuOverlayLayout(this);
super.onServiceConnected();
mA11yMenuLayout.toggleVisibility();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {}
@Override
public void onInterrupt() {
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.model;
import com.android.systemui.accessibility.accessibilitymenu.R;
/** Provides a data structure for a11y menu shortcuts. */
public class A11yMenuShortcut {
public enum ShortcutId {
UNSPECIFIED_ID_VALUE,
ID_ASSISTANT_VALUE,
ID_A11YSETTING_VALUE,
ID_POWER_VALUE,
ID_VOLUME_DOWN_VALUE,
ID_VOLUME_UP_VALUE,
ID_RECENT_VALUE,
ID_BRIGHTNESS_DOWN_VALUE,
ID_BRIGHTNESS_UP_VALUE,
ID_LOCKSCREEN_VALUE,
ID_QUICKSETTING_VALUE,
ID_NOTIFICATION_VALUE,
ID_SCREENSHOT_VALUE
}
private static final String TAG = "A11yMenuShortcut";
/** Shortcut id used to identify. */
private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal();
// Resource IDs of shortcut button and label.
public int imageSrc;
public int imageColor;
public int imgContentDescription;
public int labelText;
public A11yMenuShortcut(int id) {
setId(id);
}
/**
* Sets Id to shortcut, checks the value first and updates shortcut resources. It will set id to
*
* @param id id set to shortcut
*/
public void setId(int id) {
mShortcutId = id;
// TODO(jonesriley) load the proper resources based on id
imageSrc = R.drawable.ic_logo_assistant_32dp;
imageColor = android.R.color.darker_gray;
imgContentDescription = R.string.empty_content;
labelText = R.string.empty_content;
}
public int getId() {
return mShortcutId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof A11yMenuShortcut)) {
return false;
}
A11yMenuShortcut targetObject = (A11yMenuShortcut) o;
return mShortcutId == targetObject.mShortcutId;
}
@Override
public int hashCode() {
return mShortcutId;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.utils;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.RippleDrawable;
import com.android.systemui.accessibility.accessibilitymenu.R;
/** Creates background drawable for a11y menu shortcut. */
public class ShortcutDrawableUtils {
/**
* To make the circular background of shortcut icons have higher resolution. The higher value of
* LENGTH is, the higher resolution of the circular background are.
*/
private static final int LENGTH = 480;
private static final int RADIUS = LENGTH / 2;
private static final int COORDINATE = LENGTH / 2;
private static final int RIPPLE_COLOR_ID = R.color.ripple_material_color;
private final Context mContext;
private final ColorStateList mRippleColorStateList;
// Placeholder of drawable to prevent NullPointerException
private final ColorDrawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
public ShortcutDrawableUtils(Context context) {
this.mContext = context;
int rippleColor = context.getColor(RIPPLE_COLOR_ID);
mRippleColorStateList = ColorStateList.valueOf(rippleColor);
}
/**
* Creates a circular drawable in specific color for shortcut.
*
* @param colorResId color resource ID
* @return drawable circular drawable
*/
public Drawable createCircularDrawable(int colorResId) {
Bitmap output = Bitmap.createBitmap(LENGTH, LENGTH, Config.ARGB_8888);
Canvas canvas = new Canvas(output);
int color = mContext.getColor(colorResId);
Paint paint = new Paint();
paint.setColor(color);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStyle(Style.FILL);
canvas.drawCircle(COORDINATE, COORDINATE, RADIUS, paint);
BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), output);
return drawable;
}
/**
* Creates an adaptive icon drawable in specific color for shortcut.
*
* @param colorResId color resource ID
* @return drawable for adaptive icon
*/
public Drawable createAdaptiveIconDrawable(int colorResId) {
Drawable circleLayer = createCircularDrawable(colorResId);
RippleDrawable rippleLayer = new RippleDrawable(mRippleColorStateList, null, null);
AdaptiveIconDrawable adaptiveIconDrawable =
new AdaptiveIconDrawable(circleLayer, mTransparentDrawable);
Drawable[] layers = {adaptiveIconDrawable, rippleLayer};
LayerDrawable drawable = new LayerDrawable(layers);
return drawable;
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.view;
import android.graphics.Rect;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.TextView;
import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
import com.android.systemui.accessibility.accessibilitymenu.utils.ShortcutDrawableUtils;
import java.util.List;
/** GridView Adapter for a11y menu overlay. */
public class A11yMenuAdapter extends BaseAdapter {
// The large scale of shortcut icon and label.
private static final float LARGE_BUTTON_SCALE = 1.5f;
private final int mLargeTextSize;
private final AccessibilityMenuService mService;
private final LayoutInflater mInflater;
private final List<A11yMenuShortcut> mShortcutDataList;
private final ShortcutDrawableUtils mShortcutDrawableUtils;
public A11yMenuAdapter(
AccessibilityMenuService service, List<A11yMenuShortcut> shortcutDataList) {
this.mService = service;
this.mShortcutDataList = shortcutDataList;
mInflater = LayoutInflater.from(service);
mShortcutDrawableUtils = new ShortcutDrawableUtils(service);
mLargeTextSize =
service.getResources().getDimensionPixelOffset(R.dimen.large_label_text_size);
}
@Override
public int getCount() {
return mShortcutDataList.size();
}
@Override
public Object getItem(int position) {
return mShortcutDataList.get(position);
}
@Override
public long getItemId(int position) {
return mShortcutDataList.get(position).getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = mInflater.inflate(R.layout.grid_item, null);
A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position);
// Sets shortcut icon and label resource.
configureShortcutView(convertView, shortcutItem);
expandIconTouchArea(convertView);
setActionForMenuShortcut(convertView);
return convertView;
}
/**
* Expand shortcut icon touch area to the border of grid item.
* The height is from the top of icon to the bottom of label.
* The width is from the left border of grid item to the right border of grid item.
*/
private void expandIconTouchArea(View convertView) {
ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
shortcutIconButton.post(
() -> {
Rect iconHitRect = new Rect();
shortcutIconButton.getHitRect(iconHitRect);
Rect labelHitRect = new Rect();
shortcutLabel.getHitRect(labelHitRect);
final int widthAdjustment = iconHitRect.left;
iconHitRect.left = 0;
iconHitRect.right += widthAdjustment;
iconHitRect.top = 0;
iconHitRect.bottom = labelHitRect.bottom;
((View) shortcutIconButton.getParent())
.setTouchDelegate(new TouchDelegate(iconHitRect, shortcutIconButton));
});
}
private void setActionForMenuShortcut(View convertView) {
ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
shortcutIconButton.setOnClickListener(
(View v) -> {
// Handles shortcut click event by AccessibilityMenuService.
// service.handleClick(v);
});
}
private void configureShortcutView(View convertView, A11yMenuShortcut shortcutItem) {
ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
// TODO: Enlarge shortcut icon & label when large button setting is on.
if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) {
// Sets empty shortcut icon and label when the shortcut is ADD_ITEM.
shortcutIconButton.setImageResource(android.R.color.transparent);
shortcutIconButton.setBackground(null);
} else {
// Sets shortcut ID as tagId, to handle menu item click in AccessibilityMenuService.
shortcutIconButton.setTag(shortcutItem.getId());
shortcutIconButton.setContentDescription(
mService.getString(shortcutItem.imgContentDescription));
shortcutLabel.setText(shortcutItem.labelText);
shortcutIconButton.setImageResource(shortcutItem.imageSrc);
shortcutIconButton.setBackground(
mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor));
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.view;
import android.graphics.Rect;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageButton;
import androidx.annotation.Nullable;
import com.android.systemui.accessibility.accessibilitymenu.R;
/**
* This class is for Accessibility menu footer layout. Handles switching between a11y menu pages.
*/
public class A11yMenuFooter {
/** Provides an interface for footer of a11yMenu. */
public interface A11yMenuFooterCallBack {
/** Calls back when user clicks the left button. */
void onLeftButtonClicked();
/** Calls back when user clicks the right button. */
void onRightButtonClicked();
}
private final FooterButtonClickListener mFooterButtonClickListener;
private ImageButton mPreviousPageBtn;
private ImageButton mNextPageBtn;
private View mTopListDivider;
private View mBottomListDivider;
private final A11yMenuFooterCallBack mCallBack;
public A11yMenuFooter(ViewGroup menuLayout, A11yMenuFooterCallBack callBack) {
this.mCallBack = callBack;
mFooterButtonClickListener = new FooterButtonClickListener();
configureFooterLayout(menuLayout);
}
public @Nullable ImageButton getPreviousPageBtn() {
return mPreviousPageBtn;
}
public @Nullable ImageButton getNextPageBtn() {
return mNextPageBtn;
}
private void configureFooterLayout(ViewGroup menuLayout) {
ViewGroup footerContainer = menuLayout.findViewById(R.id.footerlayout);
footerContainer.setVisibility(View.VISIBLE);
mPreviousPageBtn = menuLayout.findViewById(R.id.menu_prev_button);
mNextPageBtn = menuLayout.findViewById(R.id.menu_next_button);
mTopListDivider = menuLayout.findViewById(R.id.top_listDivider);
mBottomListDivider = menuLayout.findViewById(R.id.bottom_listDivider);
// Registers listeners for footer buttons.
setListener(mPreviousPageBtn);
setListener(mNextPageBtn);
menuLayout
.getViewTreeObserver()
.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
menuLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
expandBtnTouchArea(mPreviousPageBtn, menuLayout);
expandBtnTouchArea(mNextPageBtn, (View) mNextPageBtn.getParent());
}
});
}
private void expandBtnTouchArea(ImageButton btn, View btnParent) {
Rect btnRect = new Rect();
btn.getHitRect(btnRect);
btnRect.top -= getHitRectHeight(mTopListDivider);
btnRect.bottom += getHitRectHeight(mBottomListDivider);
btnParent.setTouchDelegate(new TouchDelegate(btnRect, btn));
}
private static int getHitRectHeight(View listDivider) {
Rect hitRect = new Rect();
listDivider.getHitRect(hitRect);
return hitRect.height();
}
private void setListener(@Nullable View view) {
if (view != null) {
view.setOnClickListener(mFooterButtonClickListener);
}
}
/** Handles click event for footer buttons. */
private class FooterButtonClickListener implements OnClickListener {
@Override
public void onClick(View view) {
if (view.getId() == R.id.menu_prev_button) {
mCallBack.onLeftButtonClicked();
} else if (view.getId() == R.id.menu_next_button) {
mCallBack.onRightButtonClicked();
}
}
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.view;
import static java.lang.Math.max;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.widget.FrameLayout;
import android.widget.Toast;
import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
import java.util.ArrayList;
import java.util.List;
/**
* Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to
* configure or update Accessibility menu layout when orientation and display size changed, and
* functions to toggle menu visibility when button clicked or screen off.
*/
public class A11yMenuOverlayLayout {
/** Predefined default shortcuts when large button setting is off. */
private static final int[] SHORTCUT_LIST_DEFAULT = {
A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
};
private final AccessibilityMenuService mService;
private final WindowManager mWindowManager;
private ViewGroup mLayout;
private WindowManager.LayoutParams mLayoutParameter;
private A11yMenuViewPager mA11yMenuViewPager;
public A11yMenuOverlayLayout(AccessibilityMenuService service) {
mService = service;
mWindowManager = mService.getSystemService(WindowManager.class);
configureLayout();
}
/** Creates Accessibility menu layout and configure layout parameters. */
public View configureLayout() {
return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX);
}
// TODO(b/78292783): Find a better way to inflate layout in the test.
/**
* Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager.
*
* @param pageIndex the index of the ViewPager to show.
*/
public View configureLayout(int pageIndex) {
int lastVisibilityState = View.GONE;
if (mLayout != null) {
lastVisibilityState = mLayout.getVisibility();
mWindowManager.removeView(mLayout);
mLayout = null;
}
if (mLayoutParameter == null) {
initLayoutParams();
}
mLayout = new FrameLayout(mService);
updateLayoutPosition();
inflateLayoutAndSetOnTouchListener(mLayout);
mA11yMenuViewPager = new A11yMenuViewPager(mService);
mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex);
mWindowManager.addView(mLayout, mLayoutParameter);
mLayout.setVisibility(lastVisibilityState);
return mLayout;
}
/** Updates view layout with new layout parameters only. */
public void updateViewLayout() {
if (mLayout == null || mLayoutParameter == null) {
return;
}
updateLayoutPosition();
mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
}
private void initLayoutParams() {
mLayoutParameter = new WindowManager.LayoutParams();
mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
mLayoutParameter.format = PixelFormat.TRANSLUCENT;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name));
}
private void inflateLayoutAndSetOnTouchListener(ViewGroup view) {
LayoutInflater inflater = LayoutInflater.from(mService);
inflater.inflate(R.layout.paged_menu, view);
view.setOnTouchListener(mService);
}
/**
* Loads shortcut data from default shortcut ID array.
*
* @return A list of default shortcuts
*/
private List<A11yMenuShortcut> createShortcutList() {
List<A11yMenuShortcut> shortcutList = new ArrayList<>();
for (int shortcutId : SHORTCUT_LIST_DEFAULT) {
shortcutList.add(new A11yMenuShortcut(shortcutId));
}
return shortcutList;
}
/** Updates a11y menu layout position by configuring layout params. */
private void updateLayoutPosition() {
Display display = mLayout.getDisplay();
final int orientation = mService.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
switch (display.getRotation()) {
case Surface.ROTATION_90:
case Surface.ROTATION_180:
mLayoutParameter.gravity =
Gravity.END | Gravity.BOTTOM
| Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
mLayout.setBackgroundResource(R.drawable.shadow_90deg);
break;
case Surface.ROTATION_0:
case Surface.ROTATION_270:
mLayoutParameter.gravity =
Gravity.START | Gravity.BOTTOM
| Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
mLayout.setBackgroundResource(R.drawable.shadow_270deg);
break;
default:
break;
}
} else {
mLayoutParameter.gravity = Gravity.BOTTOM;
mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT;
mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT;
mLayout.setBackgroundResource(R.drawable.shadow_0deg);
}
// Adjusts the y position of a11y menu layout to make the layout not to overlap bottom
// navigation bar window.
updateLayoutByWindowInsetsIfNeeded();
mLayout.setOnApplyWindowInsetsListener(
(view, insets) -> {
if (updateLayoutByWindowInsetsIfNeeded()) {
mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
}
return view.onApplyWindowInsets(insets);
});
}
/**
* Returns {@code true} if the a11y menu layout params
* should be updated by {@link WindowManager} immediately due to window insets change.
* This method adjusts the layout position and size to
* make a11y menu not to overlap navigation bar window.
*/
private boolean updateLayoutByWindowInsetsIfNeeded() {
boolean shouldUpdateLayout = false;
WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
int xOffset = max(windowInsets.left, windowInsets.right);
int yOffset = windowInsets.bottom;
Rect windowBound = windowMetrics.getBounds();
if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) {
mLayoutParameter.x = xOffset;
mLayoutParameter.y = yOffset;
shouldUpdateLayout = true;
}
// for gestural navigation mode and the landscape mode,
// the layout height should be decreased by system bar
// and display cutout inset to fit the new
// frame size that doesn't overlap the navigation bar window.
int orientation = mService.getResources().getConfiguration().orientation;
if (mLayout.getHeight() != mLayoutParameter.height
&& orientation == Configuration.ORIENTATION_LANDSCAPE) {
mLayoutParameter.height = windowBound.height() - yOffset;
shouldUpdateLayout = true;
}
return shouldUpdateLayout;
}
/**
* Gets the current page index when device configuration changed. {@link
* AccessibilityMenuService#onConfigurationChanged(Configuration)}
*
* @return the current index of the ViewPager.
*/
public int getPageIndex() {
if (mA11yMenuViewPager != null) {
return mA11yMenuViewPager.mViewPager.getCurrentItem();
}
return A11yMenuViewPager.DEFAULT_PAGE_INDEX;
}
/**
* Hides a11y menu layout. And return if layout visibility has been changed.
*
* @return {@code true} layout visibility is toggled off; {@code false} is unchanged
*/
public boolean hideMenu() {
if (mLayout.getVisibility() == View.VISIBLE) {
mLayout.setVisibility(View.GONE);
return true;
}
return false;
}
/** Toggles a11y menu layout visibility. */
public void toggleVisibility() {
mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE);
}
/** Shows hint text on Toast. */
public void showToast(String text) {
final View viewPos = mLayout.findViewById(R.id.coordinatorLayout);
Toast.makeText(viewPos.getContext(), text, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,356 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.view;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.widget.GridView;
import androidx.viewpager.widget.ViewPager;
import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack;
import java.util.ArrayList;
import java.util.List;
/**
* This class handles UI for viewPager and footer.
* It displays grid pages containing all shortcuts in viewPager,
* and handles the click events from footer to switch between pages.
*/
public class A11yMenuViewPager {
/** The default index of the ViewPager. */
public static final int DEFAULT_PAGE_INDEX = 0;
/**
* The class holds the static parameters for grid view when large button settings is on/off.
*/
public static final class GridViewParams {
/** Total shortcuts count in the grid view when large button settings is off. */
public static final int GRID_ITEM_COUNT = 9;
/** The number of columns in the grid view when large button settings is off. */
public static final int GRID_COLUMN_COUNT = 3;
/** Total shortcuts count in the grid view when large button settings is on. */
public static final int LARGE_GRID_ITEM_COUNT = 4;
/** The number of columns in the grid view when large button settings is on. */
public static final int LARGE_GRID_COLUMN_COUNT = 2;
/** Temporary measure to test both item types. */
private static final boolean USE_LARGE_ITEMS = true;
/**
* Returns the number of items in the grid view.
*
* @param context The parent context
* @return Grid item count
*/
public static int getGridItemCount(Context context) {
return USE_LARGE_ITEMS
? LARGE_GRID_ITEM_COUNT
: GRID_ITEM_COUNT;
}
/**
* Returns the number of columns in the grid view.
*
* @param context The parent context
* @return Grid column count
*/
public static int getGridColumnCount(Context context) {
return USE_LARGE_ITEMS
? LARGE_GRID_COLUMN_COUNT
: GRID_COLUMN_COUNT;
}
/**
* Returns the number of rows in the grid view.
*
* @param context The parent context
* @return Grid row count
*/
public static int getGridRowCount(Context context) {
return USE_LARGE_ITEMS
? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT)
: (GRID_ITEM_COUNT / GRID_COLUMN_COUNT);
}
/**
* Separates a provided list of accessibility shortcuts into multiple sub-lists.
* Does not modify the original list.
*
* @param pageItemCount The maximum size of an individual sub-list.
* @param shortcutList The list of shortcuts to be separated into sub-lists.
* @return A list of shortcut sub-lists.
*/
public static List<List<A11yMenuShortcut>> generateShortcutSubLists(
int pageItemCount, List<A11yMenuShortcut> shortcutList) {
int start = 0;
int end;
int shortcutListSize = shortcutList.size();
List<List<A11yMenuShortcut>> subLists = new ArrayList<>();
while (start < shortcutListSize) {
end = Math.min(start + pageItemCount, shortcutListSize);
subLists.add(shortcutList.subList(start, end));
start = end;
}
return subLists;
}
private GridViewParams() {}
}
private final AccessibilityMenuService mService;
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next gridView pages.
*/
protected ViewPager mViewPager;
private ViewPagerAdapter<GridView> mViewPagerAdapter;
private final List<GridView> mGridPageList = new ArrayList<>();
/** The footer, which provides buttons to switch between pages */
protected A11yMenuFooter mA11yMenuFooter;
/** The shortcut list intended to show in grid pages of viewPager */
private List<A11yMenuShortcut> mA11yMenuShortcutList;
/** The container layout for a11y menu. */
private ViewGroup mA11yMenuLayout;
public A11yMenuViewPager(AccessibilityMenuService service) {
this.mService = service;
}
/**
* Configures UI for view pager and footer.
*
* @param a11yMenuLayout the container layout for a11y menu
* @param shortcutDataList the data list need to show in view pager
* @param pageIndex the index of ViewPager to show
*/
public void configureViewPagerAndFooter(
ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) {
this.mA11yMenuLayout = a11yMenuLayout;
mA11yMenuShortcutList = shortcutDataList;
initViewPager();
initChildPage();
mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks);
updateFooterState();
registerOnGlobalLayoutListener();
goToPage(pageIndex);
}
/** Initializes viewPager and its adapter. */
private void initViewPager() {
mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager);
mViewPagerAdapter = new ViewPagerAdapter<>();
mViewPager.setAdapter(mViewPagerAdapter);
mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
mViewPager.addOnPageChangeListener(
new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrollStateChanged(int state) {}
@Override
public void onPageScrolled(
int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
updateFooterState();
}
});
}
/** Creates child pages of viewPager by the length of shortcuts and initializes them. */
private void initChildPage() {
if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) {
return;
}
if (!mGridPageList.isEmpty()) {
mGridPageList.clear();
}
// Generate pages by calculating # of items per grid.
for (List<A11yMenuShortcut> page : GridViewParams.generateShortcutSubLists(
GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList)
) {
addGridPage(page);
}
mViewPagerAdapter.set(mGridPageList);
}
private void addGridPage(List<A11yMenuShortcut> shortcutDataListInPage) {
LayoutInflater inflater = LayoutInflater.from(mService);
View view = inflater.inflate(R.layout.grid_view, null);
GridView gridView = view.findViewById(R.id.gridview);
A11yMenuAdapter adapter = new A11yMenuAdapter(mService, shortcutDataListInPage);
gridView.setNumColumns(GridViewParams.getGridColumnCount(mService));
gridView.setAdapter(adapter);
mGridPageList.add(gridView);
}
/** Updates footer's state by index of current page in view pager. */
private void updateFooterState() {
int currentPage = mViewPager.getCurrentItem();
int lastPage = mViewPager.getAdapter().getCount() - 1;
mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0);
mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage);
}
private void goToPage(int pageIndex) {
if (mViewPager == null) {
return;
}
if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getCount())) {
mViewPager.setCurrentItem(pageIndex);
}
}
/** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */
private void registerOnGlobalLayoutListener() {
mA11yMenuLayout
.getViewTreeObserver()
.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
boolean mIsFirstTime = true;
@Override
public void onGlobalLayout() {
if (!mIsFirstTime) {
return;
}
if (mGridPageList.isEmpty()) {
return;
}
GridView firstGridView = mGridPageList.get(0);
if (firstGridView == null
|| firstGridView.getChildAt(0) == null) {
return;
}
mIsFirstTime = false;
int gridItemHeight = firstGridView.getChildAt(0)
.getMeasuredHeight();
adjustMenuUISize(gridItemHeight);
}
});
}
/**
* Adjusts menu UI to fit both landscape and portrait mode.
*
* <ol>
* <li>Adjust view pager's height.
* <li>Adjust vertical interval between grid items.
* <li>Adjust padding in view pager.
* </ol>
*/
private void adjustMenuUISize(int gridItemHeight) {
final int rowsInGridView = GridViewParams.getGridRowCount(mService);
final int defaultMargin =
(int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin);
final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top);
final int displayMode = mService.getResources().getConfiguration().orientation;
int viewPagerHeight = mViewPager.getMeasuredHeight();
if (displayMode == Configuration.ORIENTATION_PORTRAIT) {
// In portrait mode, we only need to adjust view pager's height to match its
// child's height.
viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin;
} else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) {
// In landscape mode, we need to adjust view pager's height to match screen height
// and adjust its child too,
// because a11y menu layout height is limited by the screen height.
DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics();
float densityScale = (float) displayMetrics.densityDpi
/ DisplayMetrics.DENSITY_DEVICE_STABLE;
View footerLayout = mA11yMenuLayout.findViewById(R.id.footerlayout);
// Keeps footer window height unchanged no matter the density is changed.
footerLayout.getLayoutParams().height =
(int) (footerLayout.getLayoutParams().height / densityScale);
// Adjust the view pager height for system bar and display cutout insets.
WindowManager windowManager = mService.getSystemService(WindowManager.class);
WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics();
Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
viewPagerHeight =
windowMetric.getBounds().height()
- footerLayout.getLayoutParams().height
- windowInsets.bottom;
// Sets vertical interval between grid items.
int interval =
(viewPagerHeight - topMargin - defaultMargin
- (rowsInGridView * gridItemHeight))
/ (rowsInGridView + 1);
for (GridView gridView : mGridPageList) {
gridView.setVerticalSpacing(interval);
}
// Sets padding to view pager.
final int finalMarginTop = interval + topMargin;
mViewPager.setPadding(defaultMargin, finalMarginTop, defaultMargin, defaultMargin);
}
final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
layoutParams.height = viewPagerHeight;
mViewPager.setLayoutParams(layoutParams);
}
/** Callback object to handle click events from A11yMenuFooter */
protected A11yMenuFooterCallBack mFooterCallbacks =
new A11yMenuFooterCallBack() {
@Override
public void onLeftButtonClicked() {
// Moves to previous page.
int targetPage = mViewPager.getCurrentItem() - 1;
goToPage(targetPage);
updateFooterState();
}
@Override
public void onRightButtonClicked() {
// Moves to next page.
int targetPage = mViewPager.getCurrentItem() + 1;
goToPage(targetPage);
updateFooterState();
}
};
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.accessibility.accessibilitymenu.view;
import android.view.View;
import android.view.ViewGroup;
import androidx.viewpager.widget.PagerAdapter;
import java.util.List;
/** The pager adapter, which provides the pages to the view pager widget. */
class ViewPagerAdapter<T extends View> extends PagerAdapter {
/** The widget list in each page of view pager. */
private List<T> mWidgetList;
ViewPagerAdapter() {}
public void set(List<T> tList) {
mWidgetList = tList;
notifyDataSetChanged();
}
@Override
public int getCount() {
if (mWidgetList == null) {
return 0;
}
return mWidgetList.size();
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mWidgetList == null) {
return null;
}
container.addView(mWidgetList.get(position));
return mWidgetList.get(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}