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