1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.privacy
18 
19 import android.app.AppOpsManager
20 import android.content.Context
21 import android.content.pm.UserInfo
22 import android.os.UserHandle
23 import com.android.internal.annotations.GuardedBy
24 import com.android.internal.annotations.VisibleForTesting
25 import com.android.systemui.appops.AppOpItem
26 import com.android.systemui.appops.AppOpsController
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Background
29 import com.android.systemui.privacy.logging.PrivacyLogger
30 import com.android.systemui.settings.UserTracker
31 import com.android.systemui.util.asIndenting
32 import com.android.systemui.util.concurrency.DelayableExecutor
33 import com.android.systemui.util.withIncreasedIndent
34 import java.io.PrintWriter
35 import javax.inject.Inject
36 
37 /**
38  * Monitors privacy items backed by app ops:
39  * - Mic & Camera
40  * - Location
41  *
42  * If [PrivacyConfig.micCameraAvailable] / [PrivacyConfig.locationAvailable] are disabled,
43  * the corresponding PrivacyItems will not be reported.
44  */
45 @SysUISingleton
46 class AppOpsPrivacyItemMonitor @Inject constructor(
47     private val appOpsController: AppOpsController,
48     private val userTracker: UserTracker,
49     private val privacyConfig: PrivacyConfig,
50     @Background private val bgExecutor: DelayableExecutor,
51     private val logger: PrivacyLogger
52 ) : PrivacyItemMonitor {
53 
54     @VisibleForTesting
55     companion object {
56         val OPS_MIC_CAMERA = intArrayOf(
57                 AppOpsManager.OP_CAMERA,
58                 AppOpsManager.OP_PHONE_CALL_CAMERA,
59                 AppOpsManager.OP_RECORD_AUDIO,
60                 AppOpsManager.OP_PHONE_CALL_MICROPHONE,
61                 AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
62                 AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
63                 AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO)
64         val OPS_LOCATION = intArrayOf(
65                 AppOpsManager.OP_COARSE_LOCATION,
66                 AppOpsManager.OP_FINE_LOCATION)
67         val OPS = OPS_MIC_CAMERA + OPS_LOCATION
68         val USER_INDEPENDENT_OPS = intArrayOf(AppOpsManager.OP_PHONE_CALL_CAMERA,
69                 AppOpsManager.OP_PHONE_CALL_MICROPHONE)
70     }
71 
72     private val lock = Any()
73 
74     @GuardedBy("lock")
75     private var callback: PrivacyItemMonitor.Callback? = null
76     @GuardedBy("lock")
77     private var micCameraAvailable = privacyConfig.micCameraAvailable
78     @GuardedBy("lock")
79     private var locationAvailable = privacyConfig.locationAvailable
80     @GuardedBy("lock")
81     private var listening = false
82 
83     private val appOpsCallback = object : AppOpsController.Callback {
84         override fun onActiveStateChanged(
85             code: Int,
86             uid: Int,
87             packageName: String,
88             active: Boolean
89         ) {
90             synchronized(lock) {
91                 // Check if we care about this code right now
92                 if (code in OPS_MIC_CAMERA && !micCameraAvailable) {
93                     return
94                 }
95                 if (code in OPS_LOCATION && !locationAvailable) {
96                     return
97                 }
98                 if (userTracker.userProfiles.any { it.id == UserHandle.getUserId(uid) } ||
99                         code in USER_INDEPENDENT_OPS) {
100                     logger.logUpdatedItemFromAppOps(code, uid, packageName, active)
101                     dispatchOnPrivacyItemsChanged()
102                 }
103             }
104         }
105     }
106 
107     @VisibleForTesting
108     internal val userTrackerCallback = object : UserTracker.Callback {
109         override fun onUserChanged(newUser: Int, userContext: Context) {
110             onCurrentProfilesChanged()
111         }
112 
113         override fun onProfilesChanged(profiles: List<UserInfo>) {
114             onCurrentProfilesChanged()
115         }
116     }
117 
118     private val configCallback = object : PrivacyConfig.Callback {
119         override fun onFlagLocationChanged(flag: Boolean) {
120             onFlagChanged()
121         }
122 
123         override fun onFlagMicCameraChanged(flag: Boolean) {
124             onFlagChanged()
125         }
126 
127         private fun onFlagChanged() {
128             synchronized(lock) {
129                 micCameraAvailable = privacyConfig.micCameraAvailable
130                 locationAvailable = privacyConfig.locationAvailable
131                 setListeningStateLocked()
132             }
133             dispatchOnPrivacyItemsChanged()
134         }
135     }
136 
137     init {
138         privacyConfig.addCallback(configCallback)
139     }
140 
141     override fun startListening(callback: PrivacyItemMonitor.Callback) {
142         synchronized(lock) {
143             this.callback = callback
144             setListeningStateLocked()
145         }
146     }
147 
148     override fun stopListening() {
149         synchronized(lock) {
150             this.callback = null
151             setListeningStateLocked()
152         }
153     }
154 
155     /**
156      * Updates listening status based on whether there are callbacks and the indicators are enabled.
157      *
158      * Always listen to all OPS so we don't have to figure out what we should be listening to. We
159      * still have to filter anyway. Updates are filtered in the callback.
160      *
161      * This is only called from private (add/remove)Callback and from the config listener, all in
162      * main thread.
163      */
164     @GuardedBy("lock")
165     private fun setListeningStateLocked() {
166         val shouldListen = callback != null && (micCameraAvailable || locationAvailable)
167         if (listening == shouldListen) {
168             return
169         }
170 
171         listening = shouldListen
172         if (shouldListen) {
173             appOpsController.addCallback(OPS, appOpsCallback)
174             userTracker.addCallback(userTrackerCallback, bgExecutor)
175             onCurrentProfilesChanged()
176         } else {
177             appOpsController.removeCallback(OPS, appOpsCallback)
178             userTracker.removeCallback(userTrackerCallback)
179         }
180     }
181 
182     override fun getActivePrivacyItems(): List<PrivacyItem> {
183         val activeAppOps = appOpsController.getActiveAppOps(true)
184         val currentUserProfiles = userTracker.userProfiles
185 
186         return synchronized(lock) {
187             activeAppOps.filter {
188                 currentUserProfiles.any { user -> user.id == UserHandle.getUserId(it.uid) } ||
189                         it.code in USER_INDEPENDENT_OPS
190             }.mapNotNull { toPrivacyItemLocked(it) }
191         }.distinct()
192     }
193 
194     @GuardedBy("lock")
195     private fun privacyItemForAppOpEnabledLocked(code: Int): Boolean {
196         if (code in OPS_LOCATION) {
197             return locationAvailable
198         } else if (code in OPS_MIC_CAMERA) {
199             return micCameraAvailable
200         } else {
201             return false
202         }
203     }
204 
205     @GuardedBy("lock")
206     private fun toPrivacyItemLocked(appOpItem: AppOpItem): PrivacyItem? {
207         if (!privacyItemForAppOpEnabledLocked(appOpItem.code)) {
208             return null
209         }
210         val type: PrivacyType = when (appOpItem.code) {
211             AppOpsManager.OP_PHONE_CALL_CAMERA,
212             AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
213             AppOpsManager.OP_COARSE_LOCATION,
214             AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
215             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
216             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
217             AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
218             AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
219             AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
220             else -> return null
221         }
222         val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
223         return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled)
224     }
225 
226     private fun onCurrentProfilesChanged() {
227         val currentUserIds = userTracker.userProfiles.map { it.id }
228         logger.logCurrentProfilesChanged(currentUserIds)
229         dispatchOnPrivacyItemsChanged()
230     }
231 
232     private fun dispatchOnPrivacyItemsChanged() {
233         val cb = synchronized(lock) { callback }
234         if (cb != null) {
235             bgExecutor.execute {
236                 cb.onPrivacyItemsChanged()
237             }
238         }
239     }
240 
241     override fun dump(pw: PrintWriter, args: Array<out String>) {
242         val ipw = pw.asIndenting()
243         ipw.println("AppOpsPrivacyItemMonitor:")
244         ipw.withIncreasedIndent {
245             synchronized(lock) {
246                 ipw.println("Listening: $listening")
247                 ipw.println("micCameraAvailable: $micCameraAvailable")
248                 ipw.println("locationAvailable: $locationAvailable")
249                 ipw.println("Callback: $callback")
250             }
251             ipw.println("Current user ids: ${userTracker.userProfiles.map { it.id }}")
252         }
253         ipw.flush()
254     }
255 }
256