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.content.pm.PackageManager
20 import android.media.projection.MediaProjectionInfo
21 import android.media.projection.MediaProjectionManager
22 import android.os.Handler
23 import android.util.Log
24 import androidx.annotation.WorkerThread
25 import com.android.internal.annotations.GuardedBy
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dagger.qualifiers.Background
28 import com.android.systemui.privacy.logging.PrivacyLogger
29 import com.android.systemui.util.asIndenting
30 import com.android.systemui.util.time.SystemClock
31 import com.android.systemui.util.withIncreasedIndent
32 import java.io.PrintWriter
33 import javax.inject.Inject
34 
35 /**
36  * Monitors the active media projection to update privacy items.
37  */
38 @SysUISingleton
39 class MediaProjectionPrivacyItemMonitor @Inject constructor(
40     private val mediaProjectionManager: MediaProjectionManager,
41     private val packageManager: PackageManager,
42     private val privacyConfig: PrivacyConfig,
43     @Background private val bgHandler: Handler,
44     private val systemClock: SystemClock,
45     private val logger: PrivacyLogger
46 ) : PrivacyItemMonitor {
47 
48     companion object {
49         const val TAG = "MediaProjectionPrivacyItemMonitor"
50         const val DEBUG = false
51     }
52 
53     private val lock = Any()
54 
55     @GuardedBy("lock")
56     private var callback: PrivacyItemMonitor.Callback? = null
57 
58     @GuardedBy("lock")
59     private var mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
60     @GuardedBy("lock")
61     private var listening = false
62 
63     @GuardedBy("lock")
64     private val privacyItems = mutableListOf<PrivacyItem>()
65 
66     private val optionsCallback = object : PrivacyConfig.Callback {
67         override fun onFlagMediaProjectionChanged(flag: Boolean) {
68             synchronized(lock) {
69                 mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
70                 setListeningStateLocked()
71             }
72             dispatchOnPrivacyItemsChanged()
73         }
74     }
75 
76     private val mediaProjectionCallback = object : MediaProjectionManager.Callback() {
77         @WorkerThread
78         override fun onStart(info: MediaProjectionInfo) {
79             synchronized(lock) { onMediaProjectionStartedLocked(info) }
80             dispatchOnPrivacyItemsChanged()
81         }
82 
83         @WorkerThread
84         override fun onStop(info: MediaProjectionInfo) {
85             synchronized(lock) { onMediaProjectionStoppedLocked(info) }
86             dispatchOnPrivacyItemsChanged()
87         }
88     }
89 
90     init {
91         privacyConfig.addCallback(optionsCallback)
92         setListeningStateLocked()
93     }
94 
95     override fun startListening(callback: PrivacyItemMonitor.Callback) {
96         synchronized(lock) {
97             this.callback = callback
98         }
99     }
100 
101     override fun stopListening() {
102         synchronized(lock) {
103             this.callback = null
104         }
105     }
106 
107     @GuardedBy("lock")
108     @WorkerThread
109     private fun onMediaProjectionStartedLocked(info: MediaProjectionInfo) {
110         if (DEBUG) Log.d(TAG, "onMediaProjectionStartedLocked: info=$info")
111         val item = makePrivacyItem(info)
112         privacyItems.add(item)
113         logItemActive(item, true)
114     }
115 
116     @GuardedBy("lock")
117     @WorkerThread
118     private fun onMediaProjectionStoppedLocked(info: MediaProjectionInfo) {
119         if (DEBUG) Log.d(TAG, "onMediaProjectionStoppedLocked: info=$info")
120         val item = makePrivacyItem(info)
121         privacyItems.removeAt(privacyItems.indexOfFirst { it.application == item.application })
122         logItemActive(item, false)
123     }
124 
125     @WorkerThread
126     private fun makePrivacyItem(info: MediaProjectionInfo): PrivacyItem {
127         val userId = info.userHandle.identifier
128         val uid = packageManager.getPackageUidAsUser(info.packageName, userId)
129         val app = PrivacyApplication(info.packageName, uid)
130         val now = systemClock.elapsedRealtime()
131         return PrivacyItem(PrivacyType.TYPE_MEDIA_PROJECTION, app, now)
132     }
133 
134     private fun logItemActive(item: PrivacyItem, active: Boolean) {
135         logger.logUpdatedItemFromMediaProjection(
136                 item.application.uid, item.application.packageName, active)
137     }
138 
139     /**
140      * Updates listening status based on whether there are callbacks and the indicator is enabled.
141      */
142     @GuardedBy("lock")
143     private fun setListeningStateLocked() {
144         val shouldListen = mediaProjectionAvailable
145         if (DEBUG) {
146             Log.d(TAG, "shouldListen=$shouldListen, " +
147                     "mediaProjectionAvailable=$mediaProjectionAvailable")
148         }
149         if (listening == shouldListen) {
150             return
151         }
152 
153         listening = shouldListen
154         if (shouldListen) {
155             if (DEBUG) Log.d(TAG, "Registering MediaProjectionManager callback")
156             mediaProjectionManager.addCallback(mediaProjectionCallback, bgHandler)
157 
158             val activeProjection = mediaProjectionManager.activeProjectionInfo
159             if (activeProjection != null) {
160                 onMediaProjectionStartedLocked(activeProjection)
161                 dispatchOnPrivacyItemsChanged()
162             }
163         } else {
164             if (DEBUG) Log.d(TAG, "Unregistering MediaProjectionManager callback")
165             mediaProjectionManager.removeCallback(mediaProjectionCallback)
166             privacyItems.forEach { logItemActive(it, false) }
167             privacyItems.clear()
168             dispatchOnPrivacyItemsChanged()
169         }
170     }
171 
172     override fun getActivePrivacyItems(): List<PrivacyItem> {
173         synchronized(lock) {
174             if (DEBUG) Log.d(TAG, "getActivePrivacyItems: privacyItems=$privacyItems")
175             return privacyItems.toList()
176         }
177     }
178 
179     private fun dispatchOnPrivacyItemsChanged() {
180         if (DEBUG) Log.d(TAG, "dispatchOnPrivacyItemsChanged")
181         val cb = synchronized(lock) { callback }
182         if (cb != null) {
183             bgHandler.post {
184                 cb.onPrivacyItemsChanged()
185             }
186         }
187     }
188 
189     override fun dump(pw: PrintWriter, args: Array<out String>) {
190         val ipw = pw.asIndenting()
191         ipw.println("MediaProjectionPrivacyItemMonitor:")
192         ipw.withIncreasedIndent {
193             synchronized(lock) {
194                 ipw.println("Listening: $listening")
195                 ipw.println("mediaProjectionAvailable: $mediaProjectionAvailable")
196                 ipw.println("Callback: $callback")
197                 ipw.println("Privacy Items: $privacyItems")
198             }
199         }
200         ipw.flush()
201     }
202 }
203