1 package com.android.systemui.qs
2 
3 import android.view.View
4 import com.android.internal.R
5 import com.android.internal.logging.UiEventLogger
6 import com.android.systemui.privacy.OngoingPrivacyChip
7 import com.android.systemui.privacy.PrivacyChipEvent
8 import com.android.systemui.privacy.PrivacyDialogController
9 import com.android.systemui.privacy.PrivacyItem
10 import com.android.systemui.privacy.PrivacyItemController
11 import com.android.systemui.privacy.logging.PrivacyLogger
12 import com.android.systemui.statusbar.phone.StatusIconContainer
13 import javax.inject.Inject
14 
15 interface ChipVisibilityListener {
16     fun onChipVisibilityRefreshed(visible: Boolean)
17 }
18 
19 /**
20  * Controls privacy icons/chip residing in QS header which show up when app is using camera,
21  * microphone or location.
22  * Manages their visibility depending on privacy signals coming from [PrivacyItemController].
23  *
24  * Unlike typical controller extending [com.android.systemui.util.ViewController] this view doesn't
25  * observe its attachment state because depending on where it is used, it might be never detached.
26  * Instead, parent controller should use [onParentVisible] and [onParentInvisible] to "activate" or
27  * "deactivate" this controller.
28  */
29 class HeaderPrivacyIconsController @Inject constructor(
30     private val privacyItemController: PrivacyItemController,
31     private val uiEventLogger: UiEventLogger,
32     private val privacyChip: OngoingPrivacyChip,
33     private val privacyDialogController: PrivacyDialogController,
34     private val privacyLogger: PrivacyLogger,
35     private val iconContainer: StatusIconContainer
36 ) {
37 
38     var chipVisibilityListener: ChipVisibilityListener? = null
39     private var listening = false
40     private var micCameraIndicatorsEnabled = false
41     private var locationIndicatorsEnabled = false
42     private var privacyChipLogged = false
43     private val cameraSlot = privacyChip.resources.getString(R.string.status_bar_camera)
44     private val micSlot = privacyChip.resources.getString(R.string.status_bar_microphone)
45     private val locationSlot = privacyChip.resources.getString(R.string.status_bar_location)
46 
47     private val picCallback: PrivacyItemController.Callback =
48             object : PrivacyItemController.Callback {
49         override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
50             privacyChip.privacyList = privacyItems
51             setChipVisibility(privacyItems.isNotEmpty())
52         }
53 
54         override fun onFlagMicCameraChanged(flag: Boolean) {
55             if (micCameraIndicatorsEnabled != flag) {
56                 micCameraIndicatorsEnabled = flag
57                 update()
58             }
59         }
60 
61         override fun onFlagLocationChanged(flag: Boolean) {
62             if (locationIndicatorsEnabled != flag) {
63                 locationIndicatorsEnabled = flag
64                 update()
65             }
66         }
67 
68         private fun update() {
69             updatePrivacyIconSlots()
70             setChipVisibility(privacyChip.privacyList.isNotEmpty())
71         }
72     }
73 
74     private fun getChipEnabled() = micCameraIndicatorsEnabled || locationIndicatorsEnabled
75 
76     fun onParentVisible() {
77         privacyChip.setOnClickListener {
78             // If the privacy chip is visible, it means there were some indicators
79             uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
80             privacyDialogController.showDialog(privacyChip.context)
81         }
82         setChipVisibility(privacyChip.visibility == View.VISIBLE)
83         micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable
84         locationIndicatorsEnabled = privacyItemController.locationAvailable
85 
86         // Ignore privacy icons because they show in the space above QQS
87         updatePrivacyIconSlots()
88     }
89 
90     fun onParentInvisible() {
91         chipVisibilityListener = null
92         privacyChip.setOnClickListener(null)
93     }
94 
95     fun startListening() {
96         listening = true
97         // Get the most up to date info
98         micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable
99         locationIndicatorsEnabled = privacyItemController.locationAvailable
100         privacyItemController.addCallback(picCallback)
101     }
102 
103     fun stopListening() {
104         listening = false
105         privacyItemController.removeCallback(picCallback)
106         privacyChipLogged = false
107     }
108 
109     private fun setChipVisibility(visible: Boolean) {
110         if (visible && getChipEnabled()) {
111             privacyLogger.logChipVisible(true)
112             // Makes sure that the chip is logged as viewed at most once each time QS is opened
113             // mListening makes sure that the callback didn't return after the user closed QS
114             if (!privacyChipLogged && listening) {
115                 privacyChipLogged = true
116                 uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW)
117             }
118         } else {
119             privacyLogger.logChipVisible(false)
120         }
121 
122         privacyChip.visibility = if (visible) View.VISIBLE else View.GONE
123         chipVisibilityListener?.onChipVisibilityRefreshed(visible)
124     }
125 
126     private fun updatePrivacyIconSlots() {
127         if (getChipEnabled()) {
128             if (micCameraIndicatorsEnabled) {
129                 iconContainer.addIgnoredSlot(cameraSlot)
130                 iconContainer.addIgnoredSlot(micSlot)
131             } else {
132                 iconContainer.removeIgnoredSlot(cameraSlot)
133                 iconContainer.removeIgnoredSlot(micSlot)
134             }
135             if (locationIndicatorsEnabled) {
136                 iconContainer.addIgnoredSlot(locationSlot)
137             } else {
138                 iconContainer.removeIgnoredSlot(locationSlot)
139             }
140         } else {
141             iconContainer.removeIgnoredSlot(cameraSlot)
142             iconContainer.removeIgnoredSlot(micSlot)
143             iconContainer.removeIgnoredSlot(locationSlot)
144         }
145     }
146 }