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