• 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.shade.display
18 
19 import android.util.Log
20 import android.view.Display
21 import android.view.MotionEvent
22 import com.android.app.tracing.coroutines.launchTraced
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Background
25 import com.android.systemui.display.data.repository.DisplayRepository
26 import com.android.systemui.shade.domain.interactor.NotificationShadeElement
27 import com.android.systemui.shade.domain.interactor.QSShadeElement
28 import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement
29 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
30 import dagger.Lazy
31 import java.util.concurrent.atomic.AtomicReference
32 import javax.inject.Inject
33 import kotlin.time.Duration.Companion.seconds
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.Job
36 import kotlinx.coroutines.delay
37 import kotlinx.coroutines.flow.MutableStateFlow
38 import kotlinx.coroutines.flow.StateFlow
39 import kotlinx.coroutines.flow.collectLatest
40 import kotlinx.coroutines.flow.distinctUntilChanged
41 import kotlinx.coroutines.flow.map
42 
43 /**
44  * Moves the shade on the last display that received a status bar touch.
45  *
46  * If the display is removed, falls back to the default one. When [shadeOnDefaultDisplayWhenLocked]
47  * is true, the shade falls back to the default display when the keyguard is visible.
48  */
49 @SysUISingleton
50 class StatusBarTouchShadeDisplayPolicy
51 @Inject
52 constructor(
53     displayRepository: DisplayRepository,
54     @Background private val backgroundScope: CoroutineScope,
55     private val qsShadeElement: Lazy<QSShadeElement>,
56     private val notificationElement: Lazy<NotificationShadeElement>,
57 ) : ShadeDisplayPolicy, ShadeExpansionIntent {
58     override val name: String = "status_bar_latest_touch"
59 
60     private val currentDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
61     private val availableDisplayIds: StateFlow<Set<Int>> = displayRepository.displayIds
62 
63     private var latestIntent = AtomicReference<ShadeElement?>()
64     private var timeoutJob: Job? = null
65 
66     override val displayId: StateFlow<Int> = currentDisplayId
67 
68     private var removalListener: Job? = null
69 
70     /** Called when the status bar on the given display is touched. */
71     fun onStatusBarTouched(event: MotionEvent, statusBarWidth: Int) {
72         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
73         updateShadeDisplayIfNeeded(event)
74         updateExpansionIntent(event, statusBarWidth)
75     }
76 
77     override fun consumeExpansionIntent(): ShadeElement? {
78         return latestIntent.getAndSet(null)
79     }
80 
81     private fun updateExpansionIntent(event: MotionEvent, statusBarWidth: Int) {
82         val element = classifyStatusBarEvent(event, statusBarWidth)
83         latestIntent.set(element)
84         timeoutJob?.cancel()
85         timeoutJob =
86             backgroundScope.launchTraced("StatusBarTouchDisplayPolicy#intentTimeout") {
87                 delay(EXPANSION_INTENT_EXPIRY)
88                 latestIntent.set(null)
89             }
90     }
91 
92     private fun updateShadeDisplayIfNeeded(event: MotionEvent) {
93         val statusBarDisplayId = event.displayId
94         if (statusBarDisplayId !in availableDisplayIds.value) {
95             Log.e(TAG, "Got touch on unknown display $statusBarDisplayId")
96             return
97         }
98         currentDisplayId.value = statusBarDisplayId
99         if (removalListener == null) {
100             // Lazy start this at the first invocation. it's fine to let it run also when the policy
101             // is not selected anymore, as the job doesn't do anything until someone subscribes to
102             // displayId.
103             removalListener = monitorDisplayRemovals()
104         }
105     }
106 
107     private fun classifyStatusBarEvent(
108         motionEvent: MotionEvent,
109         statusbarWidth: Int,
110     ): ShadeElement {
111         val xPercentage = motionEvent.x / statusbarWidth
112         return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get()
113     }
114 
115     private fun monitorDisplayRemovals(): Job {
116         return backgroundScope.launchTraced("StatusBarTouchDisplayPolicy#monitorDisplayRemovals") {
117             currentDisplayId.subscriptionCount
118                 .map { it > 0 }
119                 .distinctUntilChanged()
120                 // When Active is false, no collect happens, and the old one is cancelled.
121                 // This is needed to prevent "availableDisplayIds" collection while nobody is
122                 // listening at the flow provided by this class.
123                 .collectLatest { active ->
124                     if (active) {
125                         availableDisplayIds.collect { availableIds ->
126                             if (currentDisplayId.value !in availableIds) {
127                                 currentDisplayId.value = Display.DEFAULT_DISPLAY
128                             }
129                         }
130                     }
131                 }
132         }
133     }
134 
135     private companion object {
136         const val TAG = "StatusBarTouchDisplayPolicy"
137         val EXPANSION_INTENT_EXPIRY = 2.seconds
138     }
139 }
140