1 /*
2  * Copyright (C) 2021 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.statusbar.events
18 
19 import android.annotation.IntRange
20 import android.content.Context
21 import android.provider.DeviceConfig
22 import android.provider.DeviceConfig.NAMESPACE_PRIVACY
23 import com.android.systemui.res.R
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dagger.qualifiers.Application
26 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
27 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
28 import com.android.systemui.privacy.PrivacyChipBuilder
29 import com.android.systemui.privacy.PrivacyItem
30 import com.android.systemui.privacy.PrivacyItemController
31 import com.android.systemui.statusbar.policy.BatteryController
32 import com.android.systemui.util.time.SystemClock
33 import javax.inject.Inject
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.Job
36 import kotlinx.coroutines.flow.launchIn
37 import kotlinx.coroutines.flow.onEach
38 
39 /**
40  * Listens for system events (battery, privacy, connectivity) and allows listeners to show status
41  * bar animations when they happen
42  */
43 @SysUISingleton
44 class SystemEventCoordinator
45 @Inject
46 constructor(
47     private val systemClock: SystemClock,
48     private val batteryController: BatteryController,
49     private val privacyController: PrivacyItemController,
50     private val context: Context,
51     @Application private val appScope: CoroutineScope,
52     connectedDisplayInteractor: ConnectedDisplayInteractor
53 ) {
54     private val onDisplayConnectedFlow =
55         connectedDisplayInteractor.connectedDisplayAddition
56 
57     private var connectedDisplayCollectionJob: Job? = null
58     private lateinit var scheduler: SystemStatusAnimationScheduler
59 
startObservingnull60     fun startObserving() {
61         batteryController.addCallback(batteryStateListener)
62         privacyController.addCallback(privacyStateListener)
63         startConnectedDisplayCollection()
64     }
65 
stopObservingnull66     fun stopObserving() {
67         batteryController.removeCallback(batteryStateListener)
68         privacyController.removeCallback(privacyStateListener)
69         connectedDisplayCollectionJob?.cancel()
70     }
71 
attachSchedulernull72     fun attachScheduler(s: SystemStatusAnimationScheduler) {
73         this.scheduler = s
74     }
75 
notifyPluggedInnull76     fun notifyPluggedIn(@IntRange(from = 0, to = 100) batteryLevel: Int) {
77         scheduler.onStatusEvent(BatteryEvent(batteryLevel))
78     }
79 
notifyPrivacyItemsEmptynull80     fun notifyPrivacyItemsEmpty() {
81         scheduler.removePersistentDot()
82     }
83 
notifyPrivacyItemsChangednull84     fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) {
85         val event = PrivacyEvent(showAnimation)
86         event.privacyItems = privacyStateListener.currentPrivacyItems
87         event.contentDescription = run {
88             val items = PrivacyChipBuilder(context, event.privacyItems).joinTypes()
89             context.getString(
90                     R.string.ongoing_privacy_chip_content_multiple_apps, items)
91         }
92         scheduler.onStatusEvent(event)
93     }
94 
startConnectedDisplayCollectionnull95     private fun startConnectedDisplayCollection() {
96         val connectedDisplayEvent = ConnectedDisplayEvent().apply {
97             contentDescription = context.getString(R.string.connected_display_icon_desc)
98         }
99         connectedDisplayCollectionJob =
100                 onDisplayConnectedFlow
101                         .onEach { scheduler.onStatusEvent(connectedDisplayEvent) }
102                         .launchIn(appScope)
103     }
104 
105     private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
106         private var plugged = false
107         private var stateKnown = false
onBatteryLevelChangednull108         override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
109             if (!stateKnown) {
110                 stateKnown = true
111                 plugged = pluggedIn
112                 notifyListeners(level)
113                 return
114             }
115 
116             if (plugged != pluggedIn) {
117                 plugged = pluggedIn
118                 notifyListeners(level)
119             }
120         }
121 
notifyListenersnull122         private fun notifyListeners(@IntRange(from = 0, to = 100) batteryLevel: Int) {
123             // We only care about the plugged in status
124             if (plugged) notifyPluggedIn(batteryLevel)
125         }
126     }
127 
128     private val privacyStateListener = object : PrivacyItemController.Callback {
129         var currentPrivacyItems = listOf<PrivacyItem>()
130         var previousPrivacyItems = listOf<PrivacyItem>()
131         var timeLastEmpty = systemClock.elapsedRealtime()
132 
onPrivacyItemsChangednull133         override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
134             if (uniqueItemsMatch(privacyItems, currentPrivacyItems)) {
135                 return
136             } else if (privacyItems.isEmpty()) {
137                 previousPrivacyItems = currentPrivacyItems
138                 timeLastEmpty = systemClock.elapsedRealtime()
139             }
140 
141             currentPrivacyItems = privacyItems
142             notifyListeners()
143         }
144 
notifyListenersnull145         private fun notifyListeners() {
146             if (currentPrivacyItems.isEmpty()) {
147                 notifyPrivacyItemsEmpty()
148             } else {
149                 val showAnimation = isChipAnimationEnabled() &&
150                     (!uniqueItemsMatch(currentPrivacyItems, previousPrivacyItems) ||
151                     systemClock.elapsedRealtime() - timeLastEmpty >= DEBOUNCE_TIME)
152                 notifyPrivacyItemsChanged(showAnimation)
153             }
154         }
155 
156         // Return true if the lists contain the same permission groups, used by the same UIDs
uniqueItemsMatchnull157         private fun uniqueItemsMatch(one: List<PrivacyItem>, two: List<PrivacyItem>): Boolean {
158             return one.map { it.application.uid to it.privacyType.permGroupName }.toSet() ==
159                 two.map { it.application.uid to it.privacyType.permGroupName }.toSet()
160         }
161 
isChipAnimationEnablednull162         private fun isChipAnimationEnabled(): Boolean {
163             val defaultValue =
164                 context.resources.getBoolean(R.bool.config_enablePrivacyChipAnimation)
165             return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, defaultValue)
166         }
167     }
168 }
169 
170 private const val DEBOUNCE_TIME = 3000L
171 private const val CHIP_ANIMATION_ENABLED = "privacy_chip_animation_enabled"
172 private const val TAG = "SystemEventCoordinator"