1 package com.android.systemui.qs 2 3 import android.content.BroadcastReceiver 4 import android.content.Context 5 import android.content.Intent 6 import android.content.IntentFilter 7 import android.permission.PermissionGroupUsage 8 import android.permission.PermissionManager 9 import android.safetycenter.SafetyCenterManager 10 import android.view.View 11 import androidx.annotation.WorkerThread 12 import com.android.internal.R 13 import com.android.internal.logging.UiEventLogger 14 import com.android.systemui.animation.ActivityTransitionAnimator 15 import com.android.systemui.appops.AppOpsController 16 import com.android.systemui.broadcast.BroadcastDispatcher 17 import com.android.systemui.flags.FeatureFlags 18 import com.android.systemui.flags.Flags 19 import com.android.systemui.plugins.ActivityStarter 20 import com.android.systemui.privacy.OngoingPrivacyChip 21 import com.android.systemui.privacy.PrivacyChipEvent 22 import com.android.systemui.privacy.PrivacyDialogController 23 import com.android.systemui.privacy.PrivacyDialogControllerV2 24 import com.android.systemui.privacy.PrivacyItem 25 import com.android.systemui.privacy.PrivacyItemController 26 import com.android.systemui.privacy.logging.PrivacyLogger 27 import com.android.systemui.statusbar.phone.StatusIconContainer 28 import java.util.concurrent.Executor 29 import javax.inject.Inject 30 import com.android.systemui.dagger.qualifiers.Background 31 import com.android.systemui.dagger.qualifiers.Main 32 import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER 33 import com.android.systemui.statusbar.policy.DeviceProvisionedController 34 import javax.inject.Named 35 36 interface ChipVisibilityListener { onChipVisibilityRefreshednull37 fun onChipVisibilityRefreshed(visible: Boolean) 38 } 39 40 /** 41 * Controls privacy icons/chip residing in QS header which show up when app is using camera, 42 * microphone or location. 43 * Manages their visibility depending on privacy signals coming from [PrivacyItemController]. 44 * 45 * Unlike typical controller extending [com.android.systemui.util.ViewController] this view doesn't 46 * observe its attachment state because depending on where it is used, it might be never detached. 47 * Instead, parent controller should use [onParentVisible] and [onParentInvisible] to "activate" or 48 * "deactivate" this controller. 49 */ 50 class HeaderPrivacyIconsController @Inject constructor( 51 private val privacyItemController: PrivacyItemController, 52 private val uiEventLogger: UiEventLogger, 53 @Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip, 54 private val privacyDialogController: PrivacyDialogController, 55 private val privacyDialogControllerV2: PrivacyDialogControllerV2, 56 private val privacyLogger: PrivacyLogger, 57 @Named(SHADE_HEADER) private val iconContainer: StatusIconContainer, 58 private val permissionManager: PermissionManager, 59 @Background private val backgroundExecutor: Executor, 60 @Main private val uiExecutor: Executor, 61 private val activityStarter: ActivityStarter, 62 private val appOpsController: AppOpsController, 63 private val broadcastDispatcher: BroadcastDispatcher, 64 private val safetyCenterManager: SafetyCenterManager, 65 private val deviceProvisionedController: DeviceProvisionedController, 66 private val featureFlags: FeatureFlags 67 ) { 68 69 var chipVisibilityListener: ChipVisibilityListener? = null 70 private var listening = false 71 private var micCameraIndicatorsEnabled = false 72 private var locationIndicatorsEnabled = false 73 private var privacyChipLogged = false 74 private var safetyCenterEnabled = false 75 private val cameraSlot = privacyChip.resources.getString(R.string.status_bar_camera) 76 private val micSlot = privacyChip.resources.getString(R.string.status_bar_microphone) 77 private val locationSlot = privacyChip.resources.getString(R.string.status_bar_location) 78 79 private val safetyCenterReceiver = object : BroadcastReceiver() { 80 override fun onReceive(context: Context, intent: Intent) { 81 safetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled() 82 } 83 } 84 85 val attachStateChangeListener = object : View.OnAttachStateChangeListener { 86 override fun onViewAttachedToWindow(v: View) { 87 broadcastDispatcher.registerReceiver( 88 safetyCenterReceiver, 89 IntentFilter(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED), 90 executor = backgroundExecutor 91 ) 92 } 93 94 override fun onViewDetachedFromWindow(v: View) { 95 broadcastDispatcher.unregisterReceiver(safetyCenterReceiver) 96 } 97 } 98 99 init { 100 backgroundExecutor.execute { 101 safetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled() 102 } 103 104 if (privacyChip.isAttachedToWindow()) { 105 broadcastDispatcher.registerReceiver( 106 safetyCenterReceiver, 107 IntentFilter(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED), 108 executor = backgroundExecutor 109 ) 110 } 111 112 privacyChip.addOnAttachStateChangeListener(attachStateChangeListener) 113 } 114 115 private val picCallback: PrivacyItemController.Callback = 116 object : PrivacyItemController.Callback { 117 override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) { 118 privacyChip.privacyList = privacyItems 119 setChipVisibility(privacyItems.isNotEmpty()) 120 } 121 122 override fun onFlagMicCameraChanged(flag: Boolean) { 123 if (micCameraIndicatorsEnabled != flag) { 124 micCameraIndicatorsEnabled = flag 125 update() 126 } 127 } 128 129 override fun onFlagLocationChanged(flag: Boolean) { 130 if (locationIndicatorsEnabled != flag) { 131 locationIndicatorsEnabled = flag 132 update() 133 } 134 } 135 136 private fun update() { 137 updatePrivacyIconSlots() 138 setChipVisibility(privacyChip.privacyList.isNotEmpty()) 139 } 140 } 141 142 private fun getChipEnabled() = micCameraIndicatorsEnabled || locationIndicatorsEnabled 143 144 fun onParentVisible() { 145 privacyChip.setOnClickListener { 146 // Do not expand dialog while device is not provisioned 147 if (!deviceProvisionedController.isDeviceProvisioned) return@setOnClickListener 148 // If the privacy chip is visible, it means there were some indicators 149 uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK) 150 if (safetyCenterEnabled) { 151 if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) { 152 privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip) 153 } else { 154 showSafetyCenter() 155 } 156 } else { 157 privacyDialogController.showDialog(privacyChip.context) 158 } 159 } 160 setChipVisibility(privacyChip.visibility == View.VISIBLE) 161 micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable 162 locationIndicatorsEnabled = privacyItemController.locationAvailable 163 164 // Ignore privacy icons because they show in the space above QQS 165 updatePrivacyIconSlots() 166 } 167 168 private fun showSafetyCenter() { 169 backgroundExecutor.execute { 170 val usage = ArrayList(permGroupUsage()) 171 privacyLogger.logUnfilteredPermGroupUsage(usage) 172 val startSafetyCenter = Intent(Intent.ACTION_VIEW_SAFETY_CENTER_QS) 173 startSafetyCenter.putParcelableArrayListExtra(PermissionManager.EXTRA_PERMISSION_USAGES, 174 usage) 175 startSafetyCenter.flags = Intent.FLAG_ACTIVITY_NEW_TASK 176 uiExecutor.execute { 177 activityStarter.startActivity(startSafetyCenter, true, 178 ActivityTransitionAnimator.Controller.fromView(privacyChip)) 179 } 180 } 181 } 182 183 @WorkerThread 184 private fun permGroupUsage(): List<PermissionGroupUsage> { 185 return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted) 186 } 187 188 fun onParentInvisible() { 189 chipVisibilityListener = null 190 privacyChip.setOnClickListener(null) 191 } 192 193 fun startListening() { 194 listening = true 195 // Get the most up to date info 196 micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable 197 locationIndicatorsEnabled = privacyItemController.locationAvailable 198 privacyItemController.addCallback(picCallback) 199 } 200 201 fun stopListening() { 202 listening = false 203 privacyItemController.removeCallback(picCallback) 204 privacyChipLogged = false 205 } 206 207 private fun setChipVisibility(visible: Boolean) { 208 if (visible && getChipEnabled()) { 209 privacyLogger.logChipVisible(true) 210 // Makes sure that the chip is logged as viewed at most once each time QS is opened 211 // mListening makes sure that the callback didn't return after the user closed QS 212 if (!privacyChipLogged && listening) { 213 privacyChipLogged = true 214 uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW) 215 } 216 } else { 217 privacyLogger.logChipVisible(false) 218 } 219 220 privacyChip.visibility = if (visible) View.VISIBLE else View.GONE 221 chipVisibilityListener?.onChipVisibilityRefreshed(visible) 222 } 223 224 private fun updatePrivacyIconSlots() { 225 if (getChipEnabled()) { 226 if (micCameraIndicatorsEnabled) { 227 iconContainer.addIgnoredSlot(cameraSlot) 228 iconContainer.addIgnoredSlot(micSlot) 229 } else { 230 iconContainer.removeIgnoredSlot(cameraSlot) 231 iconContainer.removeIgnoredSlot(micSlot) 232 } 233 if (locationIndicatorsEnabled) { 234 iconContainer.addIgnoredSlot(locationSlot) 235 } else { 236 iconContainer.removeIgnoredSlot(locationSlot) 237 } 238 } else { 239 iconContainer.removeIgnoredSlot(cameraSlot) 240 iconContainer.removeIgnoredSlot(micSlot) 241 iconContainer.removeIgnoredSlot(locationSlot) 242 } 243 } 244 }