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.content.Context
20 import android.provider.DeviceConfig
21 import android.provider.DeviceConfig.NAMESPACE_PRIVACY
22 import com.android.systemui.R
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.privacy.PrivacyChipBuilder
25 import com.android.systemui.privacy.PrivacyItem
26 import com.android.systemui.privacy.PrivacyItemController
27 import com.android.systemui.statusbar.policy.BatteryController
28 import com.android.systemui.util.time.SystemClock
29 import javax.inject.Inject
30 
31 /**
32  * Listens for system events (battery, privacy, connectivity) and allows listeners
33  * to show status bar animations when they happen
34  */
35 @SysUISingleton
36 class SystemEventCoordinator @Inject constructor(
37     private val systemClock: SystemClock,
38     private val batteryController: BatteryController,
39     private val privacyController: PrivacyItemController,
40     private val context: Context
41 ) {
42     private lateinit var scheduler: SystemStatusAnimationScheduler
43 
44     fun startObserving() {
45         /* currently unused
46         batteryController.addCallback(batteryStateListener)
47         */
48         privacyController.addCallback(privacyStateListener)
49     }
50 
51     fun stopObserving() {
52         /* currently unused
53         batteryController.removeCallback(batteryStateListener)
54         */
55         privacyController.removeCallback(privacyStateListener)
56     }
57 
58     fun attachScheduler(s: SystemStatusAnimationScheduler) {
59         this.scheduler = s
60     }
61 
62     fun notifyPluggedIn() {
63         scheduler.onStatusEvent(BatteryEvent())
64     }
65 
66     fun notifyPrivacyItemsEmpty() {
67         scheduler.setShouldShowPersistentPrivacyIndicator(false)
68     }
69 
70     fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) {
71         val event = PrivacyEvent(showAnimation)
72         event.privacyItems = privacyStateListener.currentPrivacyItems
73         event.contentDescription = {
74             val items = PrivacyChipBuilder(context, event.privacyItems).joinTypes()
75             context.getString(
76                     R.string.ongoing_privacy_chip_content_multiple_apps, items)
77         }()
78         scheduler.onStatusEvent(event)
79     }
80 
81     private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
82         var plugged = false
83         var stateKnown = false
84         override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
85             if (!stateKnown) {
86                 stateKnown = true
87                 plugged = pluggedIn
88                 notifyListeners()
89                 return
90             }
91 
92             if (plugged != pluggedIn) {
93                 plugged = pluggedIn
94                 notifyListeners()
95             }
96         }
97 
98         private fun notifyListeners() {
99             // We only care about the plugged in status
100             if (plugged) notifyPluggedIn()
101         }
102     }
103 
104     private val privacyStateListener = object : PrivacyItemController.Callback {
105         var currentPrivacyItems = listOf<PrivacyItem>()
106         var previousPrivacyItems = listOf<PrivacyItem>()
107         var timeLastEmpty = systemClock.elapsedRealtime()
108 
109         override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
110             if (uniqueItemsMatch(privacyItems, currentPrivacyItems)) {
111                 return
112             } else if (privacyItems.isEmpty()) {
113                 previousPrivacyItems = currentPrivacyItems
114                 timeLastEmpty = systemClock.elapsedRealtime()
115             }
116 
117             currentPrivacyItems = privacyItems
118             notifyListeners()
119         }
120 
121         private fun notifyListeners() {
122             if (currentPrivacyItems.isEmpty()) {
123                 notifyPrivacyItemsEmpty()
124             } else {
125                 val showAnimation = isChipAnimationEnabled() &&
126                     (!uniqueItemsMatch(currentPrivacyItems, previousPrivacyItems) ||
127                     systemClock.elapsedRealtime() - timeLastEmpty >= DEBOUNCE_TIME)
128                 notifyPrivacyItemsChanged(showAnimation)
129             }
130         }
131 
132         // Return true if the lists contain the same permission groups, used by the same UIDs
133         private fun uniqueItemsMatch(one: List<PrivacyItem>, two: List<PrivacyItem>): Boolean {
134             return one.map { it.application.uid to it.privacyType.permGroupName }.toSet() ==
135                 two.map { it.application.uid to it.privacyType.permGroupName }.toSet()
136         }
137 
138         private fun isChipAnimationEnabled(): Boolean {
139             return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, true)
140         }
141     }
142 }
143 
144 private const val DEBOUNCE_TIME = 3000L
145 private const val CHIP_ANIMATION_ENABLED = "privacy_chip_animation_enabled"
146 private const val TAG = "SystemEventCoordinator"