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