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