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