| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.events |
| |
| import android.annotation.IntRange |
| import android.content.Context |
| import android.provider.DeviceConfig |
| import android.provider.DeviceConfig.NAMESPACE_PRIVACY |
| import com.android.systemui.R |
| import com.android.systemui.dagger.SysUISingleton |
| import com.android.systemui.flags.FeatureFlags |
| import com.android.systemui.flags.Flags |
| import com.android.systemui.privacy.PrivacyChipBuilder |
| import com.android.systemui.privacy.PrivacyItem |
| import com.android.systemui.privacy.PrivacyItemController |
| import com.android.systemui.statusbar.policy.BatteryController |
| import com.android.systemui.util.time.SystemClock |
| import javax.inject.Inject |
| |
| /** |
| * Listens for system events (battery, privacy, connectivity) and allows listeners |
| * to show status bar animations when they happen |
| */ |
| @SysUISingleton |
| class SystemEventCoordinator @Inject constructor( |
| private val systemClock: SystemClock, |
| private val batteryController: BatteryController, |
| private val privacyController: PrivacyItemController, |
| private val context: Context, |
| private val featureFlags: FeatureFlags |
| ) { |
| private lateinit var scheduler: SystemStatusAnimationScheduler |
| |
| fun startObserving() { |
| batteryController.addCallback(batteryStateListener) |
| privacyController.addCallback(privacyStateListener) |
| } |
| |
| fun stopObserving() { |
| batteryController.removeCallback(batteryStateListener) |
| privacyController.removeCallback(privacyStateListener) |
| } |
| |
| fun attachScheduler(s: SystemStatusAnimationScheduler) { |
| this.scheduler = s |
| } |
| |
| fun notifyPluggedIn(@IntRange(from = 0, to = 100) batteryLevel: Int) { |
| if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) { |
| scheduler.onStatusEvent(BatteryEvent(batteryLevel)) |
| } |
| } |
| |
| fun notifyPrivacyItemsEmpty() { |
| scheduler.setShouldShowPersistentPrivacyIndicator(false) |
| } |
| |
| fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) { |
| val event = PrivacyEvent(showAnimation) |
| event.privacyItems = privacyStateListener.currentPrivacyItems |
| event.contentDescription = run { |
| val items = PrivacyChipBuilder(context, event.privacyItems).joinTypes() |
| context.getString( |
| R.string.ongoing_privacy_chip_content_multiple_apps, items) |
| } |
| scheduler.onStatusEvent(event) |
| } |
| |
| private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback { |
| private var plugged = false |
| private var stateKnown = false |
| override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { |
| if (!stateKnown) { |
| stateKnown = true |
| plugged = pluggedIn |
| notifyListeners(level) |
| return |
| } |
| |
| if (plugged != pluggedIn) { |
| plugged = pluggedIn |
| notifyListeners(level) |
| } |
| } |
| |
| private fun notifyListeners(@IntRange(from = 0, to = 100) batteryLevel: Int) { |
| // We only care about the plugged in status |
| if (plugged) notifyPluggedIn(batteryLevel) |
| } |
| } |
| |
| private val privacyStateListener = object : PrivacyItemController.Callback { |
| var currentPrivacyItems = listOf<PrivacyItem>() |
| var previousPrivacyItems = listOf<PrivacyItem>() |
| var timeLastEmpty = systemClock.elapsedRealtime() |
| |
| override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) { |
| if (uniqueItemsMatch(privacyItems, currentPrivacyItems)) { |
| return |
| } else if (privacyItems.isEmpty()) { |
| previousPrivacyItems = currentPrivacyItems |
| timeLastEmpty = systemClock.elapsedRealtime() |
| } |
| |
| currentPrivacyItems = privacyItems |
| notifyListeners() |
| } |
| |
| private fun notifyListeners() { |
| if (currentPrivacyItems.isEmpty()) { |
| notifyPrivacyItemsEmpty() |
| } else { |
| val showAnimation = isChipAnimationEnabled() && |
| (!uniqueItemsMatch(currentPrivacyItems, previousPrivacyItems) || |
| systemClock.elapsedRealtime() - timeLastEmpty >= DEBOUNCE_TIME) |
| notifyPrivacyItemsChanged(showAnimation) |
| } |
| } |
| |
| // Return true if the lists contain the same permission groups, used by the same UIDs |
| private fun uniqueItemsMatch(one: List<PrivacyItem>, two: List<PrivacyItem>): Boolean { |
| return one.map { it.application.uid to it.privacyType.permGroupName }.toSet() == |
| two.map { it.application.uid to it.privacyType.permGroupName }.toSet() |
| } |
| |
| private fun isChipAnimationEnabled(): Boolean { |
| return DeviceConfig.getBoolean(NAMESPACE_PRIVACY, CHIP_ANIMATION_ENABLED, true) |
| } |
| } |
| } |
| |
| private const val DEBOUNCE_TIME = 3000L |
| private const val CHIP_ANIMATION_ENABLED = "privacy_chip_animation_enabled" |
| private const val TAG = "SystemEventCoordinator" |