• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }