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