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 }