blob: 33c47cc082e1a70979eec5e4d2341cd8d0922c92 [file] [log] [blame]
package com.android.systemui.qs
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
import android.safetycenter.SafetyCenterManager
import android.view.View
import androidx.annotation.WorkerThread
import com.android.internal.R
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer
import java.util.concurrent.Executor
import javax.inject.Inject
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import javax.inject.Named
interface ChipVisibilityListener {
fun onChipVisibilityRefreshed(visible: Boolean)
}
/**
* Controls privacy icons/chip residing in QS header which show up when app is using camera,
* microphone or location.
* Manages their visibility depending on privacy signals coming from [PrivacyItemController].
*
* Unlike typical controller extending [com.android.systemui.util.ViewController] this view doesn't
* observe its attachment state because depending on where it is used, it might be never detached.
* Instead, parent controller should use [onParentVisible] and [onParentInvisible] to "activate" or
* "deactivate" this controller.
*/
class HeaderPrivacyIconsController @Inject constructor(
private val privacyItemController: PrivacyItemController,
private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController,
private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager,
@Background private val backgroundExecutor: Executor,
@Main private val uiExecutor: Executor,
private val activityStarter: ActivityStarter,
private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager,
private val deviceProvisionedController: DeviceProvisionedController
) {
var chipVisibilityListener: ChipVisibilityListener? = null
private var listening = false
private var micCameraIndicatorsEnabled = false
private var locationIndicatorsEnabled = false
private var privacyChipLogged = false
private var safetyCenterEnabled = false
private val cameraSlot = privacyChip.resources.getString(R.string.status_bar_camera)
private val micSlot = privacyChip.resources.getString(R.string.status_bar_microphone)
private val locationSlot = privacyChip.resources.getString(R.string.status_bar_location)
private val safetyCenterReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
safetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled()
}
}
val attachStateChangeListener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
broadcastDispatcher.registerReceiver(
safetyCenterReceiver,
IntentFilter(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED),
executor = backgroundExecutor
)
}
override fun onViewDetachedFromWindow(v: View) {
broadcastDispatcher.unregisterReceiver(safetyCenterReceiver)
}
}
init {
backgroundExecutor.execute {
safetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled()
}
if (privacyChip.isAttachedToWindow()) {
broadcastDispatcher.registerReceiver(
safetyCenterReceiver,
IntentFilter(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED),
executor = backgroundExecutor
)
}
privacyChip.addOnAttachStateChangeListener(attachStateChangeListener)
}
private val picCallback: PrivacyItemController.Callback =
object : PrivacyItemController.Callback {
override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
privacyChip.privacyList = privacyItems
setChipVisibility(privacyItems.isNotEmpty())
}
override fun onFlagMicCameraChanged(flag: Boolean) {
if (micCameraIndicatorsEnabled != flag) {
micCameraIndicatorsEnabled = flag
update()
}
}
override fun onFlagLocationChanged(flag: Boolean) {
if (locationIndicatorsEnabled != flag) {
locationIndicatorsEnabled = flag
update()
}
}
private fun update() {
updatePrivacyIconSlots()
setChipVisibility(privacyChip.privacyList.isNotEmpty())
}
}
private fun getChipEnabled() = micCameraIndicatorsEnabled || locationIndicatorsEnabled
fun onParentVisible() {
privacyChip.setOnClickListener {
// Do not expand dialog while device is not provisioned
if (!deviceProvisionedController.isDeviceProvisioned) return@setOnClickListener
// If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
showSafetyCenter()
} else {
privacyDialogController.showDialog(privacyChip.context)
}
}
setChipVisibility(privacyChip.visibility == View.VISIBLE)
micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable
locationIndicatorsEnabled = privacyItemController.locationAvailable
// Ignore privacy icons because they show in the space above QQS
updatePrivacyIconSlots()
}
private fun showSafetyCenter() {
backgroundExecutor.execute {
val usage = ArrayList(permGroupUsage())
privacyLogger.logUnfilteredPermGroupUsage(usage)
val startSafetyCenter = Intent(Intent.ACTION_VIEW_SAFETY_CENTER_QS)
startSafetyCenter.putParcelableArrayListExtra(PermissionManager.EXTRA_PERMISSION_USAGES,
usage)
startSafetyCenter.flags = Intent.FLAG_ACTIVITY_NEW_TASK
uiExecutor.execute {
activityStarter.startActivity(startSafetyCenter, true,
ActivityLaunchAnimator.Controller.fromView(privacyChip))
}
}
}
@WorkerThread
private fun permGroupUsage(): List<PermissionGroupUsage> {
return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
}
fun onParentInvisible() {
chipVisibilityListener = null
privacyChip.setOnClickListener(null)
}
fun startListening() {
listening = true
// Get the most up to date info
micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable
locationIndicatorsEnabled = privacyItemController.locationAvailable
privacyItemController.addCallback(picCallback)
}
fun stopListening() {
listening = false
privacyItemController.removeCallback(picCallback)
privacyChipLogged = false
}
private fun setChipVisibility(visible: Boolean) {
if (visible && getChipEnabled()) {
privacyLogger.logChipVisible(true)
// Makes sure that the chip is logged as viewed at most once each time QS is opened
// mListening makes sure that the callback didn't return after the user closed QS
if (!privacyChipLogged && listening) {
privacyChipLogged = true
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW)
}
} else {
privacyLogger.logChipVisible(false)
}
privacyChip.visibility = if (visible) View.VISIBLE else View.GONE
chipVisibilityListener?.onChipVisibilityRefreshed(visible)
}
private fun updatePrivacyIconSlots() {
if (getChipEnabled()) {
if (micCameraIndicatorsEnabled) {
iconContainer.addIgnoredSlot(cameraSlot)
iconContainer.addIgnoredSlot(micSlot)
} else {
iconContainer.removeIgnoredSlot(cameraSlot)
iconContainer.removeIgnoredSlot(micSlot)
}
if (locationIndicatorsEnabled) {
iconContainer.addIgnoredSlot(locationSlot)
} else {
iconContainer.removeIgnoredSlot(locationSlot)
}
} else {
iconContainer.removeIgnoredSlot(cameraSlot)
iconContainer.removeIgnoredSlot(micSlot)
iconContainer.removeIgnoredSlot(locationSlot)
}
}
}