• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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(AppOpsManager.OP_CAMERA,
57                 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
58                 AppOpsManager.OP_PHONE_CALL_MICROPHONE,
59                 AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
60         val OPS_LOCATION = intArrayOf(
61                 AppOpsManager.OP_COARSE_LOCATION,
62                 AppOpsManager.OP_FINE_LOCATION)
63         val OPS = OPS_MIC_CAMERA + OPS_LOCATION
64         val USER_INDEPENDENT_OPS = intArrayOf(AppOpsManager.OP_PHONE_CALL_CAMERA,
65                 AppOpsManager.OP_PHONE_CALL_MICROPHONE)
66     }
67 
68     private val lock = Any()
69 
70     @GuardedBy("lock")
71     private var callback: PrivacyItemMonitor.Callback? = null
72     @GuardedBy("lock")
73     private var micCameraAvailable = privacyConfig.micCameraAvailable
74     @GuardedBy("lock")
75     private var locationAvailable = privacyConfig.locationAvailable
76     @GuardedBy("lock")
77     private var listening = false
78 
79     private val appOpsCallback = object : AppOpsController.Callback {
80         override fun onActiveStateChanged(
81             code: Int,
82             uid: Int,
83             packageName: String,
84             active: Boolean
85         ) {
86             synchronized(lock) {
87                 // Check if we care about this code right now
88                 if (code in OPS_MIC_CAMERA && !micCameraAvailable) {
89                     return
90                 }
91                 if (code in OPS_LOCATION && !locationAvailable) {
92                     return
93                 }
94                 if (userTracker.userProfiles.any { it.id == UserHandle.getUserId(uid) } ||
95                         code in USER_INDEPENDENT_OPS) {
96                     logger.logUpdatedItemFromAppOps(code, uid, packageName, active)
97                     dispatchOnPrivacyItemsChanged()
98                 }
99             }
100         }
101     }
102 
103     @VisibleForTesting
104     internal val userTrackerCallback = object : UserTracker.Callback {
105         override fun onUserChanged(newUser: Int, userContext: Context) {
106             onCurrentProfilesChanged()
107         }
108 
109         override fun onProfilesChanged(profiles: List<UserInfo>) {
110             onCurrentProfilesChanged()
111         }
112     }
113 
114     private val configCallback = object : PrivacyConfig.Callback {
115         override fun onFlagLocationChanged(flag: Boolean) {
116             onFlagChanged()
117         }
118 
119         override fun onFlagMicCameraChanged(flag: Boolean) {
120             onFlagChanged()
121         }
122 
123         private fun onFlagChanged() {
124             synchronized(lock) {
125                 micCameraAvailable = privacyConfig.micCameraAvailable
126                 locationAvailable = privacyConfig.locationAvailable
127                 setListeningStateLocked()
128             }
129             dispatchOnPrivacyItemsChanged()
130         }
131     }
132 
133     init {
134         privacyConfig.addCallback(configCallback)
135     }
136 
137     override fun startListening(callback: PrivacyItemMonitor.Callback) {
138         synchronized(lock) {
139             this.callback = callback
140             setListeningStateLocked()
141         }
142     }
143 
144     override fun stopListening() {
145         synchronized(lock) {
146             this.callback = null
147             setListeningStateLocked()
148         }
149     }
150 
151     /**
152      * Updates listening status based on whether there are callbacks and the indicators are enabled.
153      *
154      * Always listen to all OPS so we don't have to figure out what we should be listening to. We
155      * still have to filter anyway. Updates are filtered in the callback.
156      *
157      * This is only called from private (add/remove)Callback and from the config listener, all in
158      * main thread.
159      */
160     @GuardedBy("lock")
161     private fun setListeningStateLocked() {
162         val shouldListen = callback != null && (micCameraAvailable || locationAvailable)
163         if (listening == shouldListen) {
164             return
165         }
166 
167         listening = shouldListen
168         if (shouldListen) {
169             appOpsController.addCallback(OPS, appOpsCallback)
170             userTracker.addCallback(userTrackerCallback, bgExecutor)
171             onCurrentProfilesChanged()
172         } else {
173             appOpsController.removeCallback(OPS, appOpsCallback)
174             userTracker.removeCallback(userTrackerCallback)
175         }
176     }
177 
178     override fun getActivePrivacyItems(): List<PrivacyItem> {
179         val activeAppOps = appOpsController.getActiveAppOps(true)
180         val currentUserProfiles = userTracker.userProfiles
181 
182         return synchronized(lock) {
183             activeAppOps.filter {
184                 currentUserProfiles.any { user -> user.id == UserHandle.getUserId(it.uid) } ||
185                         it.code in USER_INDEPENDENT_OPS
186             }.mapNotNull { toPrivacyItemLocked(it) }
187         }.distinct()
188     }
189 
190     @GuardedBy("lock")
191     private fun privacyItemForAppOpEnabledLocked(code: Int): Boolean {
192         if (code in OPS_LOCATION) {
193             return locationAvailable
194         } else if (code in OPS_MIC_CAMERA) {
195             return micCameraAvailable
196         } else {
197             return false
198         }
199     }
200 
201     @GuardedBy("lock")
202     private fun toPrivacyItemLocked(appOpItem: AppOpItem): PrivacyItem? {
203         if (!privacyItemForAppOpEnabledLocked(appOpItem.code)) {
204             return null
205         }
206         val type: PrivacyType = when (appOpItem.code) {
207             AppOpsManager.OP_PHONE_CALL_CAMERA,
208             AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
209             AppOpsManager.OP_COARSE_LOCATION,
210             AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
211             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
212             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
213             AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
214             else -> return null
215         }
216         val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
217         return PrivacyItem(type, app, appOpItem.timeStartedElapsed, appOpItem.isDisabled)
218     }
219 
220     private fun onCurrentProfilesChanged() {
221         val currentUserIds = userTracker.userProfiles.map { it.id }
222         logger.logCurrentProfilesChanged(currentUserIds)
223         dispatchOnPrivacyItemsChanged()
224     }
225 
226     private fun dispatchOnPrivacyItemsChanged() {
227         val cb = synchronized(lock) { callback }
228         if (cb != null) {
229             bgExecutor.execute {
230                 cb.onPrivacyItemsChanged()
231             }
232         }
233     }
234 
235     override fun dump(pw: PrintWriter, args: Array<out String>) {
236         val ipw = pw.asIndenting()
237         ipw.println("AppOpsPrivacyItemMonitor:")
238         ipw.withIncreasedIndent {
239             synchronized(lock) {
240                 ipw.println("Listening: $listening")
241                 ipw.println("micCameraAvailable: $micCameraAvailable")
242                 ipw.println("locationAvailable: $locationAvailable")
243                 ipw.println("Callback: $callback")
244             }
245             ipw.println("Current user ids: ${userTracker.userProfiles.map { it.id }}")
246         }
247         ipw.flush()
248     }
249 }
250