Loading packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +5 −0 Original line number Original line Diff line number Diff line Loading @@ -40,6 +40,8 @@ import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepos import com.android.systemui.display.data.repository.DisplaysWithDecorationsRepositoryImpl import com.android.systemui.display.data.repository.DisplaysWithDecorationsRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.data.repository.KioskModeRepository import com.android.systemui.display.data.repository.KioskModeRepositoryImpl import com.android.systemui.display.data.repository.PerDisplayRepoDumpHelper import com.android.systemui.display.data.repository.PerDisplayRepoDumpHelper import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl Loading Loading @@ -77,6 +79,9 @@ interface DisplayModule { deviceStateRepository: DeviceStateRepositoryImpl deviceStateRepository: DeviceStateRepositoryImpl ): DeviceStateRepository ): DeviceStateRepository @Binds fun bindsKioskModeRepository(kioskModeRepository: KioskModeRepositoryImpl): KioskModeRepository @Binds @Binds fun bindsFocusedDisplayRepository( fun bindsFocusedDisplayRepository( focusedDisplayRepository: FocusedDisplayRepositoryImpl focusedDisplayRepository: FocusedDisplayRepositoryImpl Loading packages/SystemUI/src/com/android/systemui/display/data/repository/KioskModeRepository.kt 0 → 100644 +71 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.display.data.repository import android.app.ActivityManager import android.app.ActivityManager.LOCK_TASK_MODE_LOCKED import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.kairos.awaitClose import com.android.systemui.shared.system.TaskStackChangeListener import com.android.systemui.shared.system.TaskStackChangeListeners import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Repository for the current kiosk mode (lock task mode) state. */ interface KioskModeRepository { /** A flow that emits `true` when the device enters kiosk mode, and `false` when it exits. */ val isInKioskMode: StateFlow<Boolean> } @SysUISingleton class KioskModeRepositoryImpl @Inject constructor( @Application private val scope: CoroutineScope, @Background private val bgContext: CoroutineContext, private val activityManager: ActivityManager, private val taskStackChangeListeners: TaskStackChangeListeners, ) : KioskModeRepository { override val isInKioskMode: StateFlow<Boolean> = conflatedCallbackFlow { val listener = object : TaskStackChangeListener { override fun onLockTaskModeChanged(mode: Int) { trySend(mode == LOCK_TASK_MODE_LOCKED) } } taskStackChangeListeners.registerTaskStackListener(listener) // When the flow is cancelled (e.g., the scope is destroyed), // unregister the listener to prevent memory leaks. awaitClose { taskStackChangeListeners.unregisterTaskStackListener(listener) } } .onStart { emit(activityManager.lockTaskModeState == LOCK_TASK_MODE_LOCKED) } .flowOn(bgContext) .stateIn(scope, SharingStarted.WhileSubscribed(), false) } packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +20 −14 Original line number Original line Diff line number Diff line Loading @@ -15,8 +15,6 @@ */ */ package com.android.systemui.display.ui.viewmodel package com.android.systemui.display.ui.viewmodel import android.app.ActivityManager import android.app.ActivityManager.LOCK_TASK_MODE_LOCKED import android.app.Dialog import android.app.Dialog import android.content.Context import android.content.Context import android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY import android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY Loading @@ -39,6 +37,7 @@ import com.android.systemui.biometrics.Utils.getInsetsOf import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.KioskModeRepository import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.ui.view.ExternalDisplayConnectionDialogDelegate import com.android.systemui.display.ui.view.ExternalDisplayConnectionDialogDelegate Loading Loading @@ -75,7 +74,7 @@ constructor( private val context: Context, private val context: Context, private val desktopState: DesktopState, private val desktopState: DesktopState, private val secureSettings: SecureSettings, private val secureSettings: SecureSettings, private val activityManager: ActivityManager, private val kioskModeRepository: KioskModeRepository, private val connectedDisplayInteractor: ConnectedDisplayInteractor, private val connectedDisplayInteractor: ConnectedDisplayInteractor, @Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @Background private val bgDispatcher: CoroutineDispatcher, Loading @@ -91,27 +90,33 @@ constructor( @OptIn(FlowPreview::class) @OptIn(FlowPreview::class) override fun start() { override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val kioskModeFlow = kioskModeRepository.isInKioskMode val concurrentDisplaysInProgressFlow = val concurrentDisplaysInProgressFlow = if (Flags.enableDualDisplayBlocking()) { if (Flags.enableDualDisplayBlocking()) { connectedDisplayInteractor.concurrentDisplaysInProgress connectedDisplayInteractor.concurrentDisplaysInProgress } else { } else { flow { emit(false) } flow { emit(false) } } } pendingDisplayFlow // Let's debounce for 2 reasons: // Let's debounce for 2 reasons: // - prevent fast dialog flashes in case pending displays are available for just a few // - prevent fast dialog flashes where pending displays are available for just a few millis // millis // - prevent jumps related to inset changes: when in 3 buttons navigation, device unlock // - Prevent jumps related to inset changes: when in 3 buttons navigation, device // triggers a change in insets that might result in a jump of the dialog (if a display was // unlock triggers a change in insets that might result in a jump of the dialog (if a // connected while on the lockscreen). // display was connected while on the lockscreen). val debouncedPendingDisplayFlow = pendingDisplayFlow.debounce(200.milliseconds) .debounce(200.milliseconds) .combine(concurrentDisplaysInProgressFlow) { combine(debouncedPendingDisplayFlow, kioskModeFlow, concurrentDisplaysInProgressFlow) { pendingDisplay, pendingDisplay, isInKioskMode, concurrentDisplaysInProgress -> concurrentDisplaysInProgress -> if (pendingDisplay == null) { if (pendingDisplay == null) { dismissDialog() dismissDialog() } else { } else { handleNewPendingDisplay(pendingDisplay, concurrentDisplaysInProgress) handleNewPendingDisplay( pendingDisplay, isInKioskMode, concurrentDisplaysInProgress, ) } } } } .launchIn(scope) .launchIn(scope) Loading Loading @@ -170,6 +175,7 @@ constructor( private suspend fun handleNewPendingDisplay( private suspend fun handleNewPendingDisplay( pendingDisplay: PendingDisplay, pendingDisplay: PendingDisplay, isInKioskMode: Boolean, concurrentDisplaysInProgress: Boolean, concurrentDisplaysInProgress: Boolean, ) { ) { val useNewDialog = val useNewDialog = Loading @@ -180,10 +186,10 @@ constructor( return return } } val isInKioskMode = activityManager.lockTaskModeState == LOCK_TASK_MODE_LOCKED val isInExtendedMode = desktopState.isDesktopModeSupportedOnDisplay(DEFAULT_DISPLAY) val isInExtendedMode = desktopState.isDesktopModeSupportedOnDisplay(DEFAULT_DISPLAY) if (isInKioskMode) { if (isInKioskMode) { dismissDialog() pendingDisplay.showNewDialog(concurrentDisplaysInProgress, isInKioskMode) pendingDisplay.showNewDialog(concurrentDisplaysInProgress, isInKioskMode) } else if (isInExtendedMode) { } else if (isInExtendedMode) { pendingDisplay.enableForDesktop() pendingDisplay.enableForDesktop() Loading Loading
packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +5 −0 Original line number Original line Diff line number Diff line Loading @@ -40,6 +40,8 @@ import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepos import com.android.systemui.display.data.repository.DisplaysWithDecorationsRepositoryImpl import com.android.systemui.display.data.repository.DisplaysWithDecorationsRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepository import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl import com.android.systemui.display.data.repository.KioskModeRepository import com.android.systemui.display.data.repository.KioskModeRepositoryImpl import com.android.systemui.display.data.repository.PerDisplayRepoDumpHelper import com.android.systemui.display.data.repository.PerDisplayRepoDumpHelper import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl Loading Loading @@ -77,6 +79,9 @@ interface DisplayModule { deviceStateRepository: DeviceStateRepositoryImpl deviceStateRepository: DeviceStateRepositoryImpl ): DeviceStateRepository ): DeviceStateRepository @Binds fun bindsKioskModeRepository(kioskModeRepository: KioskModeRepositoryImpl): KioskModeRepository @Binds @Binds fun bindsFocusedDisplayRepository( fun bindsFocusedDisplayRepository( focusedDisplayRepository: FocusedDisplayRepositoryImpl focusedDisplayRepository: FocusedDisplayRepositoryImpl Loading
packages/SystemUI/src/com/android/systemui/display/data/repository/KioskModeRepository.kt 0 → 100644 +71 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.display.data.repository import android.app.ActivityManager import android.app.ActivityManager.LOCK_TASK_MODE_LOCKED import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.kairos.awaitClose import com.android.systemui.shared.system.TaskStackChangeListener import com.android.systemui.shared.system.TaskStackChangeListeners import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Repository for the current kiosk mode (lock task mode) state. */ interface KioskModeRepository { /** A flow that emits `true` when the device enters kiosk mode, and `false` when it exits. */ val isInKioskMode: StateFlow<Boolean> } @SysUISingleton class KioskModeRepositoryImpl @Inject constructor( @Application private val scope: CoroutineScope, @Background private val bgContext: CoroutineContext, private val activityManager: ActivityManager, private val taskStackChangeListeners: TaskStackChangeListeners, ) : KioskModeRepository { override val isInKioskMode: StateFlow<Boolean> = conflatedCallbackFlow { val listener = object : TaskStackChangeListener { override fun onLockTaskModeChanged(mode: Int) { trySend(mode == LOCK_TASK_MODE_LOCKED) } } taskStackChangeListeners.registerTaskStackListener(listener) // When the flow is cancelled (e.g., the scope is destroyed), // unregister the listener to prevent memory leaks. awaitClose { taskStackChangeListeners.unregisterTaskStackListener(listener) } } .onStart { emit(activityManager.lockTaskModeState == LOCK_TASK_MODE_LOCKED) } .flowOn(bgContext) .stateIn(scope, SharingStarted.WhileSubscribed(), false) }
packages/SystemUI/src/com/android/systemui/display/ui/viewmodel/ConnectingDisplayViewModel.kt +20 −14 Original line number Original line Diff line number Diff line Loading @@ -15,8 +15,6 @@ */ */ package com.android.systemui.display.ui.viewmodel package com.android.systemui.display.ui.viewmodel import android.app.ActivityManager import android.app.ActivityManager.LOCK_TASK_MODE_LOCKED import android.app.Dialog import android.app.Dialog import android.content.Context import android.content.Context import android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY import android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY Loading @@ -39,6 +37,7 @@ import com.android.systemui.biometrics.Utils.getInsetsOf import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.KioskModeRepository import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay import com.android.systemui.display.ui.view.ExternalDisplayConnectionDialogDelegate import com.android.systemui.display.ui.view.ExternalDisplayConnectionDialogDelegate Loading Loading @@ -75,7 +74,7 @@ constructor( private val context: Context, private val context: Context, private val desktopState: DesktopState, private val desktopState: DesktopState, private val secureSettings: SecureSettings, private val secureSettings: SecureSettings, private val activityManager: ActivityManager, private val kioskModeRepository: KioskModeRepository, private val connectedDisplayInteractor: ConnectedDisplayInteractor, private val connectedDisplayInteractor: ConnectedDisplayInteractor, @Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @Background private val bgDispatcher: CoroutineDispatcher, Loading @@ -91,27 +90,33 @@ constructor( @OptIn(FlowPreview::class) @OptIn(FlowPreview::class) override fun start() { override fun start() { val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val pendingDisplayFlow = connectedDisplayInteractor.pendingDisplay val kioskModeFlow = kioskModeRepository.isInKioskMode val concurrentDisplaysInProgressFlow = val concurrentDisplaysInProgressFlow = if (Flags.enableDualDisplayBlocking()) { if (Flags.enableDualDisplayBlocking()) { connectedDisplayInteractor.concurrentDisplaysInProgress connectedDisplayInteractor.concurrentDisplaysInProgress } else { } else { flow { emit(false) } flow { emit(false) } } } pendingDisplayFlow // Let's debounce for 2 reasons: // Let's debounce for 2 reasons: // - prevent fast dialog flashes in case pending displays are available for just a few // - prevent fast dialog flashes where pending displays are available for just a few millis // millis // - prevent jumps related to inset changes: when in 3 buttons navigation, device unlock // - Prevent jumps related to inset changes: when in 3 buttons navigation, device // triggers a change in insets that might result in a jump of the dialog (if a display was // unlock triggers a change in insets that might result in a jump of the dialog (if a // connected while on the lockscreen). // display was connected while on the lockscreen). val debouncedPendingDisplayFlow = pendingDisplayFlow.debounce(200.milliseconds) .debounce(200.milliseconds) .combine(concurrentDisplaysInProgressFlow) { combine(debouncedPendingDisplayFlow, kioskModeFlow, concurrentDisplaysInProgressFlow) { pendingDisplay, pendingDisplay, isInKioskMode, concurrentDisplaysInProgress -> concurrentDisplaysInProgress -> if (pendingDisplay == null) { if (pendingDisplay == null) { dismissDialog() dismissDialog() } else { } else { handleNewPendingDisplay(pendingDisplay, concurrentDisplaysInProgress) handleNewPendingDisplay( pendingDisplay, isInKioskMode, concurrentDisplaysInProgress, ) } } } } .launchIn(scope) .launchIn(scope) Loading Loading @@ -170,6 +175,7 @@ constructor( private suspend fun handleNewPendingDisplay( private suspend fun handleNewPendingDisplay( pendingDisplay: PendingDisplay, pendingDisplay: PendingDisplay, isInKioskMode: Boolean, concurrentDisplaysInProgress: Boolean, concurrentDisplaysInProgress: Boolean, ) { ) { val useNewDialog = val useNewDialog = Loading @@ -180,10 +186,10 @@ constructor( return return } } val isInKioskMode = activityManager.lockTaskModeState == LOCK_TASK_MODE_LOCKED val isInExtendedMode = desktopState.isDesktopModeSupportedOnDisplay(DEFAULT_DISPLAY) val isInExtendedMode = desktopState.isDesktopModeSupportedOnDisplay(DEFAULT_DISPLAY) if (isInKioskMode) { if (isInKioskMode) { dismissDialog() pendingDisplay.showNewDialog(concurrentDisplaysInProgress, isInKioskMode) pendingDisplay.showNewDialog(concurrentDisplaysInProgress, isInKioskMode) } else if (isInExtendedMode) { } else if (isInExtendedMode) { pendingDisplay.enableForDesktop() pendingDisplay.enableForDesktop() Loading