• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 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.server.wm;
18 
19 import static android.content.Context.MEDIA_PROJECTION_SERVICE;
20 
21 import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
22 
23 import android.media.projection.IMediaProjectionManager;
24 import android.media.projection.IMediaProjectionWatcherCallback;
25 import android.media.projection.MediaProjectionEvent;
26 import android.media.projection.MediaProjectionInfo;
27 import android.os.Binder;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.os.ServiceManager;
31 import android.util.ArrayMap;
32 import android.util.ArraySet;
33 import android.view.ContentRecordingSession;
34 import android.window.IScreenRecordingCallback;
35 
36 import com.android.internal.annotations.GuardedBy;
37 import com.android.internal.protolog.ProtoLog;
38 
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 
42 public class ScreenRecordingCallbackController {
43 
44     private final class Callback implements IBinder.DeathRecipient {
45 
46         IScreenRecordingCallback mCallback;
47         int mUid;
48 
Callback(IScreenRecordingCallback callback, int uid)49         Callback(IScreenRecordingCallback callback, int uid) {
50             this.mCallback = callback;
51             this.mUid = uid;
52         }
53 
binderDied()54         public void binderDied() {
55             unregister(mCallback);
56         }
57     }
58 
59     @GuardedBy("WindowManagerService.mGlobalLock")
60     private final ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<>();
61 
62     @GuardedBy("WindowManagerService.mGlobalLock")
63     private final ArrayMap<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>();
64 
65     private final WindowManagerService mWms;
66 
67     @GuardedBy("WindowManagerService.mGlobalLock")
68     private WindowContainer<WindowContainer> mRecordedWC;
69 
70     private boolean mWatcherCallbackRegistered = false;
71 
72     private final class MediaProjectionWatcherCallback extends
73             IMediaProjectionWatcherCallback.Stub {
74         @Override
onStart(MediaProjectionInfo mediaProjectionInfo)75         public void onStart(MediaProjectionInfo mediaProjectionInfo) {
76             onScreenRecordingStart(mediaProjectionInfo);
77         }
78 
79         @Override
onStop(MediaProjectionInfo mediaProjectionInfo)80         public void onStop(MediaProjectionInfo mediaProjectionInfo) {
81             onScreenRecordingStop();
82         }
83 
84         @Override
onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, ContentRecordingSession contentRecordingSession)85         public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo,
86                 ContentRecordingSession contentRecordingSession) {
87         }
88 
89         @Override
onMediaProjectionEvent( MediaProjectionEvent event, MediaProjectionInfo mediaProjectionInfo, ContentRecordingSession session)90         public void onMediaProjectionEvent(
91                 MediaProjectionEvent event,
92                 MediaProjectionInfo mediaProjectionInfo,
93                 ContentRecordingSession session) {}
94     }
95 
ScreenRecordingCallbackController(WindowManagerService wms)96     ScreenRecordingCallbackController(WindowManagerService wms) {
97         mWms = wms;
98     }
99 
100     @GuardedBy("WindowManagerService.mGlobalLock")
setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo)101     private void setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo) {
102         if (mediaProjectionInfo.getLaunchCookie() == null) {
103             mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay();
104         } else {
105             final ActivityRecord matchingActivity = mWms.mRoot.getActivity(activity ->
106                     activity.mLaunchCookie == mediaProjectionInfo.getLaunchCookie().binder);
107             mRecordedWC = matchingActivity != null ? matchingActivity.getTask() : null;
108         }
109     }
110 
111     @GuardedBy("WindowManagerService.mGlobalLock")
ensureMediaProjectionWatcherCallbackRegistered()112     private void ensureMediaProjectionWatcherCallbackRegistered() {
113         if (mWatcherCallbackRegistered) {
114             return;
115         }
116 
117         IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
118         IMediaProjectionManager mediaProjectionManager = IMediaProjectionManager.Stub.asInterface(
119                 binder);
120 
121         long identityToken = Binder.clearCallingIdentity();
122         MediaProjectionInfo mediaProjectionInfo = null;
123         try {
124             mediaProjectionInfo = mediaProjectionManager.addCallback(
125                     new MediaProjectionWatcherCallback());
126             mWatcherCallbackRegistered = true;
127         } catch (RemoteException e) {
128             ProtoLog.e(WM_ERROR, "Failed to register MediaProjectionWatcherCallback");
129         } finally {
130             Binder.restoreCallingIdentity(identityToken);
131         }
132 
133         if (mediaProjectionInfo != null) {
134             setRecordedWindowContainer(mediaProjectionInfo);
135         }
136     }
137 
register(IScreenRecordingCallback callback)138     boolean register(IScreenRecordingCallback callback) {
139         synchronized (mWms.mGlobalLock) {
140             ensureMediaProjectionWatcherCallbackRegistered();
141 
142             IBinder binder = callback.asBinder();
143             int uid = Binder.getCallingUid();
144 
145             if (mCallbacks.containsKey(binder)) {
146                 return mLastInvokedStateByUid.get(uid);
147             }
148 
149             Callback callbackInfo = new Callback(callback, uid);
150             try {
151                 binder.linkToDeath(callbackInfo, 0);
152             } catch (RemoteException e) {
153                 return false;
154             }
155 
156             boolean uidInRecording = uidHasRecordedActivity(callbackInfo.mUid);
157             mLastInvokedStateByUid.put(callbackInfo.mUid, uidInRecording);
158             mCallbacks.put(binder, callbackInfo);
159             return uidInRecording;
160         }
161     }
162 
unregister(IScreenRecordingCallback callback)163     void unregister(IScreenRecordingCallback callback) {
164         synchronized (mWms.mGlobalLock) {
165             IBinder binder = callback.asBinder();
166             Callback callbackInfo = mCallbacks.remove(binder);
167             if (callbackInfo == null) {
168                 return;
169             }
170             binder.unlinkToDeath(callbackInfo, 0);
171 
172             boolean uidHasCallback = false;
173             for (int i = 0; i < mCallbacks.size(); i++) {
174                 if (mCallbacks.valueAt(i).mUid == callbackInfo.mUid) {
175                     uidHasCallback = true;
176                     break;
177                 }
178             }
179             if (!uidHasCallback) {
180                 mLastInvokedStateByUid.remove(callbackInfo.mUid);
181             }
182         }
183     }
184 
onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo)185     private void onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo) {
186         synchronized (mWms.mGlobalLock) {
187             setRecordedWindowContainer(mediaProjectionInfo);
188             dispatchCallbacks(getRecordedUids(), true /* visibleInScreenRecording*/);
189         }
190     }
191 
onScreenRecordingStop()192     private void onScreenRecordingStop() {
193         synchronized (mWms.mGlobalLock) {
194             dispatchCallbacks(getRecordedUids(), false /*visibleInScreenRecording*/);
195             mRecordedWC = null;
196         }
197     }
198 
199     @GuardedBy("WindowManagerService.mGlobalLock")
onProcessActivityVisibilityChanged(int uid, boolean processVisible)200     void onProcessActivityVisibilityChanged(int uid, boolean processVisible) {
201         // If recording isn't active or there's no registered callback for the uid, there's nothing
202         // to do on this visibility change.
203         if (mRecordedWC == null || !mLastInvokedStateByUid.containsKey(uid)) {
204             return;
205         }
206 
207         // If the callbacks are already in the correct state, avoid making duplicate callbacks for
208         // the same state. This can happen when:
209         // * a process becomes visible but its UID already has a recorded activity from another
210         //   process.
211         // * a process becomes invisible but its UID already doesn't have any recorded activities.
212         if (processVisible == mLastInvokedStateByUid.get(uid)) {
213             return;
214         }
215 
216         // If the process visibility change doesn't change the visibility of the UID, avoid making
217         // duplicate callbacks for the same state. This can happen when:
218         // * a process becomes visible but the newly visible activity isn't in the recorded window
219         //   container.
220         // * a process becomes invisible but there are still activities being recorded for the UID.
221         boolean uidInRecording = uidHasRecordedActivity(uid);
222         if ((processVisible && !uidInRecording) || (!processVisible && uidInRecording)) {
223             return;
224         }
225 
226         ArraySet<Integer> uidSet = new ArraySet<>();
227         uidSet.add(uid);
228         dispatchCallbacks(uidSet, processVisible);
229     }
230 
231     @GuardedBy("WindowManagerService.mGlobalLock")
uidHasRecordedActivity(int uid)232     private boolean uidHasRecordedActivity(int uid) {
233         if (mRecordedWC == null) {
234             return false;
235         }
236         boolean[] hasRecordedActivity = {false};
237         mRecordedWC.forAllActivities(activityRecord -> {
238             if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) {
239                 hasRecordedActivity[0] = true;
240                 return true;
241             }
242             return false;
243         }, true /*traverseTopToBottom*/);
244         return hasRecordedActivity[0];
245     }
246 
247     @GuardedBy("WindowManagerService.mGlobalLock")
getRecordedUids()248     private ArraySet<Integer> getRecordedUids() {
249         ArraySet<Integer> result = new ArraySet<>();
250         if (mRecordedWC == null) {
251             return result;
252         }
253         mRecordedWC.forAllActivities(activityRecord -> {
254             if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey(
255                     activityRecord.getUid())) {
256                 result.add(activityRecord.getUid());
257             }
258         }, true /*traverseTopToBottom*/);
259         return result;
260     }
261 
262     @GuardedBy("WindowManagerService.mGlobalLock")
dispatchCallbacks(ArraySet<Integer> uids, boolean visibleInScreenRecording)263     private void dispatchCallbacks(ArraySet<Integer> uids, boolean visibleInScreenRecording) {
264         if (uids.isEmpty()) {
265             return;
266         }
267 
268         for (int i = 0; i < uids.size(); i++) {
269             mLastInvokedStateByUid.put(uids.valueAt(i), visibleInScreenRecording);
270         }
271 
272         ArrayList<IScreenRecordingCallback> callbacks = new ArrayList<>();
273         for (int i = 0; i < mCallbacks.size(); i++) {
274             if (uids.contains(mCallbacks.valueAt(i).mUid)) {
275                 callbacks.add(mCallbacks.valueAt(i).mCallback);
276             }
277         }
278 
279         mWms.mH.post(() -> {
280             for (int i = 0; i < callbacks.size(); i++) {
281                 try {
282                     callbacks.get(i).onScreenRecordingStateChanged(visibleInScreenRecording);
283                 } catch (RemoteException e) {
284                     // Client has died. Cleanup is handled via DeathRecipient.
285                 }
286             }
287         });
288     }
289 
dump(PrintWriter pw)290     void dump(PrintWriter pw) {
291         pw.format("ScreenRecordingCallbackController:\n");
292         pw.format("  Registered callbacks:\n");
293         for (int i = 0; i < mCallbacks.size(); i++) {
294             pw.format("    callback=%s uid=%s\n", mCallbacks.keyAt(i), mCallbacks.valueAt(i).mUid);
295         }
296         pw.format("  Last invoked states:\n");
297         for (int i = 0; i < mLastInvokedStateByUid.size(); i++) {
298             pw.format("    uid=%s isVisibleInScreenRecording=%s\n", mLastInvokedStateByUid.keyAt(i),
299                     mLastInvokedStateByUid.valueAt(i));
300         }
301     }
302 }
303