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"