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