1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.keyboard.shortcut.ui.view
18
19 import android.content.ActivityNotFoundException
20 import android.content.Intent
21 import android.graphics.Insets
22 import android.os.Bundle
23 import android.provider.Settings
24 import android.view.View
25 import android.view.WindowInsets
26 import androidx.activity.BackEventCompat
27 import androidx.activity.ComponentActivity
28 import androidx.activity.OnBackPressedCallback
29 import androidx.compose.ui.platform.ComposeView
30 import androidx.core.view.updatePadding
31 import androidx.lifecycle.flowWithLifecycle
32 import androidx.lifecycle.lifecycleScope
33 import com.android.compose.theme.PlatformTheme
34 import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
35 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
36 import com.android.systemui.res.R
37 import com.google.android.material.bottomsheet.BottomSheetBehavior
38 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
39 import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
40 import javax.inject.Inject
41 import kotlinx.coroutines.launch
42
43 /**
44 * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
45 * small and large screen devices.
46 */
47 class ShortcutHelperActivity
48 @Inject
49 constructor(
50 private val viewModel: ShortcutHelperViewModel,
51 ) : ComponentActivity() {
52
53 private val bottomSheetContainer
54 get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
55
56 private val bottomSheet
57 get() = requireViewById<View>(R.id.shortcut_helper_sheet)
58
59 private val bottomSheetBehavior
60 get() = BottomSheetBehavior.from(bottomSheet)
61
62 override fun onCreate(savedInstanceState: Bundle?) {
63 setupEdgeToEdge()
64 super.onCreate(savedInstanceState)
65 setContentView(R.layout.activity_keyboard_shortcut_helper)
66 setUpBottomSheetWidth()
67 expandBottomSheet()
68 setUpInsets()
69 setUpPredictiveBack()
70 setUpSheetDismissListener()
71 setUpDismissOnTouchOutside()
72 setUpComposeView()
73 observeFinishRequired()
74 viewModel.onViewOpened()
75 }
76
77 private fun setUpComposeView() {
78 requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
79 setContent {
80 PlatformTheme {
81 ShortcutHelper(
82 onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
83 )
84 }
85 }
86 }
87 }
88
89 private fun onKeyboardSettingsClicked() {
90 try {
91 startActivity(Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS))
92 } catch (e: ActivityNotFoundException) {
93 // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
94 // you safeguard against this.
95 e.printStackTrace()
96 }
97 }
98
99 override fun onDestroy() {
100 super.onDestroy()
101 if (isFinishing) {
102 viewModel.onViewClosed()
103 }
104 }
105
106 private fun observeFinishRequired() {
107 lifecycleScope.launch {
108 viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
109 if (!shouldShow) {
110 finish()
111 }
112 }
113 }
114 }
115
116 private fun setupEdgeToEdge() {
117 // Draw behind system bars
118 window.setDecorFitsSystemWindows(false)
119 }
120
121 private fun setUpBottomSheetWidth() {
122 val sheetScreenWidthFraction =
123 resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
124 // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
125 // effect.
126 val screenWidth = resources.displayMetrics.widthPixels
127 bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
128 }
129
130 private fun setUpInsets() {
131 bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
132 val safeDrawingInsets = insets.safeDrawing
133 // Make sure the bottom sheet is not covered by the status bar.
134 bottomSheetBehavior.maxHeight =
135 resources.displayMetrics.heightPixels - safeDrawingInsets.top
136 // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
137 // cutouts.
138 bottomSheet.updatePadding(
139 left = safeDrawingInsets.left,
140 right = safeDrawingInsets.right,
141 bottom = safeDrawingInsets.bottom
142 )
143 // The bottom sheet has to be expanded only after setting up insets, otherwise there is
144 // a bug and it will not use full height.
145 expandBottomSheet()
146
147 // Return CONSUMED if you don't want want the window insets to keep passing
148 // down to descendant views.
149 WindowInsets.CONSUMED
150 }
151 }
152
153 private fun expandBottomSheet() {
154 bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
155 bottomSheetBehavior.skipCollapsed = true
156 }
157
158 private fun setUpPredictiveBack() {
159 val onBackPressedCallback =
160 object : OnBackPressedCallback(/* enabled= */ true) {
161 override fun handleOnBackStarted(backEvent: BackEventCompat) {
162 bottomSheetBehavior.startBackProgress(backEvent)
163 }
164
165 override fun handleOnBackProgressed(backEvent: BackEventCompat) {
166 bottomSheetBehavior.updateBackProgress(backEvent)
167 }
168
169 override fun handleOnBackPressed() {
170 bottomSheetBehavior.handleBackInvoked()
171 }
172
173 override fun handleOnBackCancelled() {
174 bottomSheetBehavior.cancelBackProgress()
175 }
176 }
177 onBackPressedDispatcher.addCallback(
178 owner = this,
179 onBackPressedCallback = onBackPressedCallback
180 )
181 }
182
183 private fun setUpSheetDismissListener() {
184 bottomSheetBehavior.addBottomSheetCallback(
185 object : BottomSheetCallback() {
186 override fun onStateChanged(bottomSheet: View, newState: Int) {
187 if (newState == STATE_HIDDEN) {
188 finish()
189 }
190 }
191
192 override fun onSlide(bottomSheet: View, slideOffset: Float) {}
193 }
194 )
195 }
196
197 private fun setUpDismissOnTouchOutside() {
198 bottomSheetContainer.setOnClickListener { finish() }
199 }
200 }
201
202 private val WindowInsets.safeDrawing
203 get() =
204 getInsets(WindowInsets.Type.systemBars())
205 .union(getInsets(WindowInsets.Type.displayCutout()))
206
Insetsnull207 private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
208