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