Shortcut Helper - "Scaffold" implementation

Implementation of the helper container and its bottom sheet

Test: Manual
Flag: NONE
Fixes: 335387426
Change-Id: Ie9930437943d24f5552cba7abd764074b458b9d6
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index bbc9fe4..de38b8e 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1033,6 +1033,7 @@
 
         <receiver
             android:name=".statusbar.KeyboardShortcutsReceiver"
+            android:visibleToInstantApps="true"
             android:exported="true">
             <intent-filter>
                 <action android:name="com.android.intent.action.DISMISS_KEYBOARD_SHORTCUTS" />
@@ -1113,5 +1114,11 @@
                 android:name="android.service.dream"
                 android:resource="@xml/home_controls_dream_metadata" />
         </service>
+
+        <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+            android:exported="false"
+            android:theme="@style/ShortcutHelperTheme"
+            android:excludeFromRecents="true"
+            android:finishOnCloseSystemDialogs="true" />
     </application>
 </manifest>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
new file mode 100644
index 0000000..47fd78a
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:anim/accelerate_interpolator"
+    android:zAdjustment="top">
+
+    <translate
+        android:fromYDelta="0"
+        android:toYDelta="100%"
+        android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
new file mode 100644
index 0000000..77edf58
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<!-- Animation for when a dock window at the bottom of the screen is entering. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+    android:zAdjustment="top">
+
+    <translate android:fromYDelta="100%"
+        android:toYDelta="0"
+        android:startOffset="@android:integer/config_shortAnimTime"
+        android:duration="@android:integer/config_mediumAnimTime"/>
+</set>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
new file mode 100644
index 0000000..292e496
--- /dev/null
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/shortcut_helper_sheet_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/shortcut_helper_sheet"
+        style="@style/Widget.Material3.BottomSheet"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+
+        <!-- Drag handle for accessibility -->
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:id="@+id/drag_handle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:textAppearance="?textAppearanceDisplayLarge"
+            android:background="?colorTertiaryContainer"
+            android:text="Shortcut Helper Content" />
+    </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 291f8a2..546bf1c 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -72,4 +72,8 @@
         <item name="android:textColor">@color/material_dynamic_secondary80</item>
     </style>
 
+    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+        <item name="android:windowLightNavigationBar">false</item>
+    </style>
+
 </resources>
diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml
new file mode 100644
index 0000000..5e4304e
--- /dev/null
+++ b/packages/SystemUI/res/values-xlarge-land/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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>
+    <item name="shortcut_helper_screen_width_fraction"  format="float" type="dimen">0.8</item>
+</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index fa9d507d..7d66ae1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1007,4 +1007,7 @@
 
     <!-- Whether volume panel should use the large screen layout or not -->
     <bool name="volume_panel_is_large_screen">false</bool>
+
+    <!-- The width of the shortcut helper container, as a fraction of the screen's width. -->
+    <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">1.0</item>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 2c9006e..3d57111 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1654,4 +1654,28 @@
     <style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog">
         <item name="android:colorBackground">@color/transparent</item>
     </style>
+
+    <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
+        <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+        <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+        <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+        <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+    </style>
+
+    <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight">
+        <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:backgroundDimEnabled">true</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+        <item name="enableEdgeToEdge">true</item>
+    </style>
+
+    <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+        <item name="android:windowLightNavigationBar">true</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
new file mode 100644
index 0000000..692fbb0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.keyboard.shortcut
+
+import android.graphics.Insets
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.updatePadding
+import com.android.systemui.res.R
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+
+/**
+ * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
+ * small and large screen devices.
+ */
+class ShortcutHelperActivity : ComponentActivity() {
+
+    private val bottomSheetContainer
+        get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
+
+    private val bottomSheet
+        get() = requireViewById<View>(R.id.shortcut_helper_sheet)
+
+    private val bottomSheetBehavior
+        get() = BottomSheetBehavior.from(bottomSheet)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setupEdgeToEdge()
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_keyboard_shortcut_helper)
+        setUpBottomSheetWidth()
+        setUpInsets()
+        setUpPredictiveBack()
+        setUpSheetDismissListener()
+        setUpDismissOnTouchOutside()
+    }
+
+    private fun setupEdgeToEdge() {
+        // Draw behind system bars
+        window.setDecorFitsSystemWindows(false)
+    }
+
+    private fun setUpBottomSheetWidth() {
+        val sheetScreenWidthFraction =
+            resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
+        // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
+        // effect.
+        val screenWidth = resources.displayMetrics.widthPixels
+        bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
+    }
+
+    private fun setUpInsets() {
+        bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
+            val safeDrawingInsets = insets.safeDrawing
+            // Make sure the bottom sheet is not covered by the status bar.
+            bottomSheetContainer.updatePadding(top = safeDrawingInsets.top)
+            // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
+            // cutouts.
+            bottomSheet.updatePadding(
+                left = safeDrawingInsets.left,
+                right = safeDrawingInsets.right,
+                bottom = safeDrawingInsets.bottom
+            )
+            // The bottom sheet has to be expanded only after setting up insets, otherwise there is
+            // a bug and it will not use full height.
+            expandBottomSheet()
+
+            // Return CONSUMED if you don't want want the window insets to keep passing
+            // down to descendant views.
+            WindowInsets.CONSUMED
+        }
+    }
+
+    private fun expandBottomSheet() {
+        bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+        bottomSheetBehavior.skipCollapsed = true
+    }
+
+    private fun setUpPredictiveBack() {
+        val onBackPressedCallback =
+            object : OnBackPressedCallback(/* enabled= */ true) {
+                override fun handleOnBackStarted(backEvent: BackEventCompat) {
+                    bottomSheetBehavior.startBackProgress(backEvent)
+                }
+
+                override fun handleOnBackProgressed(backEvent: BackEventCompat) {
+                    bottomSheetBehavior.updateBackProgress(backEvent)
+                }
+
+                override fun handleOnBackPressed() {
+                    bottomSheetBehavior.handleBackInvoked()
+                }
+
+                override fun handleOnBackCancelled() {
+                    bottomSheetBehavior.cancelBackProgress()
+                }
+            }
+        onBackPressedDispatcher.addCallback(
+            owner = this,
+            onBackPressedCallback = onBackPressedCallback
+        )
+    }
+
+    private fun setUpSheetDismissListener() {
+        bottomSheetBehavior.addBottomSheetCallback(
+            object : BottomSheetCallback() {
+                override fun onStateChanged(bottomSheet: View, newState: Int) {
+                    if (newState == STATE_HIDDEN) {
+                        finish()
+                    }
+                }
+
+                override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+            }
+        )
+    }
+
+    private fun setUpDismissOnTouchOutside() {
+        bottomSheetContainer.setOnClickListener { finish() }
+    }
+}
+
+private val WindowInsets.safeDrawing
+    get() =
+        getInsets(WindowInsets.Type.systemBars())
+            .union(getInsets(WindowInsets.Type.ime()))
+            .union(getInsets(WindowInsets.Type.displayCutout()))
+
+private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)