• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.statusbar.policy.ui.dialog
18 
19 import android.annotation.UiThread
20 import android.app.Dialog
21 import android.content.Context
22 import android.content.Intent
23 import android.provider.Settings
24 import android.util.Log
25 import androidx.compose.foundation.isSystemInDarkTheme
26 import androidx.compose.material3.Text
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.remember
29 import androidx.compose.ui.Modifier
30 import androidx.compose.ui.platform.testTag
31 import androidx.compose.ui.res.stringResource
32 import androidx.compose.ui.semantics.paneTitle
33 import androidx.compose.ui.semantics.semantics
34 import androidx.compose.ui.semantics.testTagsAsResourceId
35 import androidx.lifecycle.DefaultLifecycleObserver
36 import androidx.lifecycle.LifecycleOwner
37 import com.android.compose.PlatformButton
38 import com.android.compose.PlatformOutlinedButton
39 import com.android.compose.theme.PlatformTheme
40 import com.android.internal.annotations.VisibleForTesting
41 import com.android.internal.jank.InteractionJankMonitor
42 import com.android.settingslib.notification.modes.EnableDndDialogFactory
43 import com.android.systemui.animation.DialogCuj
44 import com.android.systemui.animation.DialogTransitionAnimator
45 import com.android.systemui.animation.Expandable
46 import com.android.systemui.dagger.SysUISingleton
47 import com.android.systemui.dagger.qualifiers.Application
48 import com.android.systemui.dagger.qualifiers.Background
49 import com.android.systemui.dagger.qualifiers.Main
50 import com.android.systemui.dialog.ui.composable.AlertDialogContent
51 import com.android.systemui.plugins.ActivityStarter
52 import com.android.systemui.qs.tiles.dialog.QSEnableDndDialogMetricsLogger
53 import com.android.systemui.res.R
54 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
55 import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
56 import com.android.systemui.statusbar.phone.SystemUIDialog
57 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
58 import com.android.systemui.statusbar.phone.create
59 import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
60 import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
61 import com.android.systemui.util.Assert
62 import javax.inject.Inject
63 import javax.inject.Provider
64 import kotlin.coroutines.CoroutineContext
65 import kotlinx.coroutines.CoroutineScope
66 import kotlinx.coroutines.launch
67 import kotlinx.coroutines.withContext
68 
69 @SysUISingleton
70 class ModesDialogDelegate
71 @Inject
72 constructor(
73     val context: Context,
74     private val sysuiDialogFactory: SystemUIDialogFactory,
75     private val dialogTransitionAnimator: DialogTransitionAnimator,
76     private val activityStarter: ActivityStarter,
77     // Using a provider to avoid a circular dependency.
78     private val viewModel: Provider<ModesDialogViewModel>,
79     private val dialogEventLogger: ModesDialogEventLogger,
80     @Application private val applicationCoroutineScope: CoroutineScope,
81     @Main private val mainCoroutineContext: CoroutineContext,
82     @Background private val bgContext: CoroutineContext,
83     private val shadeDisplayContextRepository: ShadeDialogContextInteractor,
84 ) : SystemUIDialog.Delegate {
85     // NOTE: This should only be accessed/written from the main thread.
86     @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null
87     private val dndDurationDialogLogger by lazy { QSEnableDndDialogMetricsLogger(context) }
88 
89     override fun createDialog(): SystemUIDialog {
90         Assert.isMainThread()
91         if (currentDialog != null) {
92             Log.w(TAG, "Dialog is already open, dismissing it and creating a new one.")
93             currentDialog?.dismiss()
94         }
95 
96         currentDialog =
97             sysuiDialogFactory.create(context = shadeDisplayContextRepository.context) {
98                 ModesDialogContent(it)
99             }
100         currentDialog
101             ?.lifecycle
102             ?.addObserver(
103                 object : DefaultLifecycleObserver {
104                     override fun onStop(owner: LifecycleOwner) {
105                         Assert.isMainThread()
106                         currentDialog = null
107                     }
108                 }
109             )
110 
111         return currentDialog!!
112     }
113 
114     @Composable
115     private fun ModesDialogContent(dialog: SystemUIDialog) {
116         // TODO(b/369376884): The composable does correctly update when the theme changes
117         //  while the dialog is open, but the background (which we don't control here)
118         //  doesn't, which causes us to show things like white text on a white background.
119         //  as a workaround, we remember the original theme and keep it on recomposition.
120         val isCurrentlyInDarkTheme = isSystemInDarkTheme()
121         val cachedDarkTheme = remember { isCurrentlyInDarkTheme }
122         PlatformTheme(isDarkTheme = cachedDarkTheme) {
123             AlertDialogContent(
124                 modifier =
125                     Modifier.semantics {
126                         testTagsAsResourceId = true
127                         paneTitle =
128                             dialog.context.getString(R.string.accessibility_desc_quick_settings)
129                     },
130                 title = {
131                     Text(
132                         modifier = Modifier.testTag("modes_title"),
133                         text = stringResource(R.string.zen_modes_dialog_title),
134                     )
135                 },
136                 content = { ModeTileGrid(viewModel.get()) },
137                 neutralButton = {
138                     PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
139                         Text(stringResource(R.string.zen_modes_dialog_settings))
140                     }
141                 },
142                 positiveButton = {
143                     PlatformButton(onClick = { dialog.dismiss() }) {
144                         Text(stringResource(R.string.zen_modes_dialog_done))
145                     }
146                 },
147             )
148         }
149     }
150 
151     @VisibleForTesting
152     fun openSettings(dialog: SystemUIDialog) {
153         dialogEventLogger.logDialogSettings()
154         val animationController =
155             dialogTransitionAnimator.createActivityTransitionController(dialog)
156         if (animationController == null) {
157             // The controller will take care of dismissing for us after
158             // the animation, but let's make sure we dismiss the dialog
159             // if we don't animate it.
160             dialog.dismiss()
161         }
162         activityStarter.startActivity(
163             ZEN_MODE_SETTINGS_INTENT,
164             /* dismissShade= */ true,
165             animationController,
166         )
167     }
168 
169     suspend fun showDialog(expandable: Expandable? = null): SystemUIDialog {
170         // Dialogs shown by the DialogTransitionAnimator must be created and shown on the main
171         // thread, so we post it to the UI handler.
172         withContext(mainCoroutineContext) {
173             // Create the dialog if necessary
174             if (currentDialog == null) {
175                 createDialog()
176             }
177 
178             expandable
179                 ?.dialogTransitionController(
180                     DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
181                 )
182                 ?.let { controller -> dialogTransitionAnimator.show(currentDialog!!, controller) }
183                 ?: currentDialog!!.show()
184         }
185 
186         return currentDialog!!
187     }
188 
189     /**
190      * Launches the [intent] by animating from the dialog. If the dialog is not showing, just
191      * launches it normally without animating.
192      */
193     fun launchFromDialog(intent: Intent) {
194         // TODO: b/394571336 - Remove this method and inline "actual" if b/394571336 fixed.
195         // Workaround for Compose bug, see b/394241061 and b/394571336 -- Need to post on the main
196         // thread so that dialog dismissal doesn't crash after a long press inside it (the *double*
197         // jump, out and back in, is because mainCoroutineContext is .immediate).
198         applicationCoroutineScope.launch {
199             withContext(bgContext) {
200                 withContext(mainCoroutineContext) { actualLaunchFromDialog(intent) }
201             }
202         }
203     }
204 
205     private fun actualLaunchFromDialog(intent: Intent) {
206         Assert.isMainThread()
207         if (currentDialog == null) {
208             Log.w(
209                 TAG,
210                 "Cannot launch from dialog, the dialog is not present. " +
211                     "Will launch activity without animating.",
212             )
213         }
214 
215         val animationController =
216             currentDialog?.let { dialogTransitionAnimator.createActivityTransitionController(it) }
217         if (animationController == null) {
218             currentDialog?.dismiss()
219         }
220         activityStarter.startActivity(intent, /* dismissShade= */ true, animationController)
221     }
222 
223     /**
224      * Special dialog to ask the user for the duration of DND. Not to be confused with the modes
225      * dialog itself.
226      */
227     @UiThread
228     fun makeDndDurationDialog(): Dialog {
229         val dialog =
230             EnableDndDialogFactory(
231                     context,
232                     R.style.Theme_SystemUI_Dialog,
233                     /* cancelIsNeutral= */ true,
234                     dndDurationDialogLogger,
235                 )
236                 .createDialog()
237         SystemUIDialog.applyFlags(dialog)
238         SystemUIDialog.setShowForAllUsers(dialog, true)
239         SystemUIDialog.registerDismissListener(dialog)
240         SystemUIDialog.setDialogSize(dialog)
241         return dialog
242     }
243 
244     companion object {
245         private const val TAG = "ModesDialogDelegate"
246         private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
247         private const val INTERACTION_JANK_TAG = "configure_priority_modes"
248     }
249 }
250