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.ActivityTransitionAnimator import com.android.systemui.appops.AppOpsController import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags 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.PrivacyDialogControllerV2 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 privacyDialogControllerV2: PrivacyDialogControllerV2, 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, private val featureFlags: FeatureFlags ) { 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) { 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) { if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) { privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip) } else { 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, ActivityTransitionAnimator.Controller.fromView(privacyChip)) } } } @WorkerThread private fun permGroupUsage(): List { 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) } } }