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"