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