• 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.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 }