1 /*
<lambda>null2  * Copyright (C) 2023 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 package com.android.systemui.display.ui.viewmodel
17 
18 import android.app.Dialog
19 import android.content.Context
20 import com.android.server.policy.feature.flags.Flags
21 import com.android.systemui.CoreStartable
22 import com.android.systemui.biometrics.Utils
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.dagger.qualifiers.Background
26 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
27 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
28 import com.android.systemui.display.ui.view.MirroringConfirmationDialogDelegate
29 import dagger.Binds
30 import dagger.Module
31 import dagger.multibindings.ClassKey
32 import dagger.multibindings.IntoMap
33 import javax.inject.Inject
34 import kotlin.time.Duration.Companion.milliseconds
35 import kotlinx.coroutines.CoroutineDispatcher
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.FlowPreview
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.debounce
40 import kotlinx.coroutines.flow.flow
41 import kotlinx.coroutines.flow.launchIn
42 import com.android.app.tracing.coroutines.launchTraced as launch
43 
44 /**
45  * Shows/hides a dialog to allow the user to decide whether to use the external display for
46  * mirroring.
47  */
48 @SysUISingleton
49 class ConnectingDisplayViewModel
50 @Inject
51 constructor(
52     private val context: Context,
53     private val connectedDisplayInteractor: ConnectedDisplayInteractor,
54     @Application private val scope: CoroutineScope,
55     @Background private val bgDispatcher: CoroutineDispatcher,
56     private val bottomSheetFactory: MirroringConfirmationDialogDelegate.Factory,
57 ) : CoreStartable {
58 
59     private var dialog: Dialog? = null
60 
61     /** Starts listening for pending displays. */
62     @OptIn(FlowPreview::class)
63     override fun start() {
64         val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay
65         val concurrentDisplaysInProgessFlow =
66             if (Flags.enableDualDisplayBlocking()) {
67                 connectedDisplayInteractor.concurrentDisplaysInProgress
68             } else {
69                 flow { emit(false) }
70             }
71         pendingDisplayFlow
72             // Let's debounce for 2 reasons:
73             // - prevent fast dialog flashes in case pending displays are available for just a few
74             // millis
75             // - Prevent jumps related to inset changes: when in 3 buttons navigation, device
76             // unlock triggers a change in insets that might result in a jump of the dialog (if a
77             // display was connected while on the lockscreen).
78             .debounce(200.milliseconds)
79             .combine(concurrentDisplaysInProgessFlow) { pendingDisplay, concurrentDisplaysInProgress
80                 ->
81                 if (pendingDisplay == null) {
82                     dismissDialog()
83                 } else {
84                     showDialog(pendingDisplay, concurrentDisplaysInProgress)
85                 }
86             }
87             .launchIn(scope)
88     }
89 
90     private fun showDialog(pendingDisplay: PendingDisplay, concurrentDisplaysInProgess: Boolean) {
91         dismissDialog()
92         dialog =
93             bottomSheetFactory
94                 .createDialog(
95                     onStartMirroringClickListener = {
96                         scope.launch(context = bgDispatcher) { pendingDisplay.enable() }
97                         dismissDialog()
98                     },
99                     onCancelMirroring = {
100                         scope.launch(context = bgDispatcher) { pendingDisplay.ignore() }
101                         dismissDialog()
102                     },
103                     navbarBottomInsetsProvider = { Utils.getNavbarInsets(context).bottom },
104                     showConcurrentDisplayInfo = concurrentDisplaysInProgess
105                 )
106                 .apply { show() }
107     }
108 
109     private fun dismissDialog() {
110         dialog?.dismiss()
111         dialog = null
112     }
113 
114     @Module
115     interface StartableModule {
116         @Binds
117         @IntoMap
118         @ClassKey(ConnectingDisplayViewModel::class)
119         fun bindsConnectingDisplayViewModel(impl: ConnectingDisplayViewModel): CoreStartable
120     }
121 }
122