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.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
20 import static android.content.Context.MEDIA_PROJECTION_SERVICE;
21 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
22 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
23 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
24 import static android.view.ViewProtoEnums.DISPLAY_STATE_OFF;
25 
26 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONTENT_RECORDING;
27 
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.content.res.Configuration;
31 import android.graphics.Point;
32 import android.graphics.PointF;
33 import android.graphics.Rect;
34 import android.media.projection.IMediaProjectionManager;
35 import android.os.IBinder;
36 import android.os.RemoteException;
37 import android.os.ServiceManager;
38 import android.view.ContentRecordingSession;
39 import android.view.ContentRecordingSession.RecordContent;
40 import android.view.Display;
41 import android.view.DisplayInfo;
42 import android.view.SurfaceControl;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.protolog.common.ProtoLog;
46 import com.android.server.display.feature.DisplayManagerFlags;
47 
48 /**
49  * Manages content recording for a particular {@link DisplayContent}.
50  */
51 final class ContentRecorder implements WindowContainerListener {
52 
53     /**
54      * Maximum acceptable anisotropy for the output image.
55      *
56      * Necessary to avoid unnecessary scaling when the anisotropy is almost the same, as it is not
57      * exact anyway. For external displays, we expect an anisoptry of about 2% even if the pixels
58      * are, in fact, square due to the imprecision of the display's actual size (rounded to the
59      * nearest cm).
60      */
61     private static final float MAX_ANISOTROPY = 0.025f;
62 
63     /**
64      * The display content this class is handling recording for.
65      */
66     @NonNull
67     private final DisplayContent mDisplayContent;
68 
69     @Nullable private final MediaProjectionManagerWrapper mMediaProjectionManager;
70 
71     /**
72      * The session for content recording, or null if this DisplayContent is not being used for
73      * recording.
74      */
75     private ContentRecordingSession mContentRecordingSession = null;
76 
77     /**
78      * The WindowContainer for the level of the hierarchy to record.
79      */
80     @Nullable private WindowContainer mRecordedWindowContainer = null;
81 
82     /**
83      * The surface for recording the contents of this hierarchy, or null if content recording is
84      * temporarily disabled.
85      */
86     @Nullable private SurfaceControl mRecordedSurface = null;
87 
88     /**
89      * The last bounds of the region to record.
90      */
91     @Nullable private Rect mLastRecordedBounds = null;
92 
93     /**
94      * The last size of the surface mirrored out to.
95      */
96     @Nullable private Point mLastConsumingSurfaceSize = new Point(0, 0);
97 
98     /**
99      * The last configuration orientation.
100      */
101     @Configuration.Orientation
102     private int mLastOrientation = ORIENTATION_UNDEFINED;
103 
104     private int mLastWindowingMode = WINDOWING_MODE_UNDEFINED;
105 
106     private final boolean mCorrectForAnisotropicPixels;
107 
ContentRecorder(@onNull DisplayContent displayContent)108     ContentRecorder(@NonNull DisplayContent displayContent) {
109         this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId),
110                 new DisplayManagerFlags().isConnectedDisplayManagementEnabled()
111                         && !new DisplayManagerFlags()
112                                     .isPixelAnisotropyCorrectionInLogicalDisplayEnabled()
113                         && displayContent.getDisplayInfo().type == Display.TYPE_EXTERNAL);
114     }
115 
116     @VisibleForTesting
ContentRecorder(@onNull DisplayContent displayContent, @NonNull MediaProjectionManagerWrapper mediaProjectionManager, boolean correctForAnisotropicPixels)117     ContentRecorder(@NonNull DisplayContent displayContent,
118             @NonNull MediaProjectionManagerWrapper mediaProjectionManager,
119             boolean correctForAnisotropicPixels) {
120         mDisplayContent = displayContent;
121         mMediaProjectionManager = mediaProjectionManager;
122         mCorrectForAnisotropicPixels = correctForAnisotropicPixels;
123     }
124 
125     /**
126      * Sets the incoming recording session. Should only be used when starting to record on
127      * this display; stopping recording is handled separately when the display is destroyed.
128      *
129      * @param session the new session indicating recording will begin on this display.
130      */
setContentRecordingSession(@ullable ContentRecordingSession session)131     void setContentRecordingSession(@Nullable ContentRecordingSession session) {
132         mContentRecordingSession = session;
133     }
134 
isContentRecordingSessionSet()135     boolean isContentRecordingSessionSet() {
136         return mContentRecordingSession != null;
137     }
138 
139     /**
140      * Returns {@code true} if this DisplayContent is currently recording.
141      */
isCurrentlyRecording()142     boolean isCurrentlyRecording() {
143         return mContentRecordingSession != null && mRecordedSurface != null;
144     }
145 
146     /**
147      * Start recording if this DisplayContent no longer has content. Pause recording if it now
148      * has content or the display is not on.
149      */
updateRecording()150     @VisibleForTesting void updateRecording() {
151         if (isCurrentlyRecording() && (mDisplayContent.getLastHasContent()
152                 || mDisplayContent.getDisplayInfo().state == Display.STATE_OFF)) {
153             pauseRecording();
154         } else {
155             // Display no longer has content, or now has a surface to write to, so try to start
156             // recording.
157             startRecordingIfNeeded();
158         }
159     }
160 
onMirrorOutputSurfaceOrientationChanged()161     void onMirrorOutputSurfaceOrientationChanged() {
162         onConfigurationChanged(mLastOrientation, mLastWindowingMode);
163     }
164 
165     /**
166      * Handle a configuration change on the display content, and resize recording if needed.
167      * @param lastOrientation the prior orientation of the configuration
168      */
onConfigurationChanged( @onfiguration.Orientation int lastOrientation, int lastWindowingMode)169     void onConfigurationChanged(
170             @Configuration.Orientation int lastOrientation, int lastWindowingMode) {
171         // Update surface for MediaProjection, if this DisplayContent is being used for recording.
172         if (!isCurrentlyRecording() || mLastRecordedBounds == null) {
173             return;
174         }
175 
176         // Recording has already begun, but update recording since the display is now on.
177         if (mRecordedWindowContainer == null) {
178             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
179                     "Content Recording: Unexpectedly null window container; unable to update "
180                             + "recording for display %d",
181                     mDisplayContent.getDisplayId());
182             return;
183         }
184 
185         // TODO(b/297514518) Do not start capture if the app is in PIP, the bounds are
186         //  inaccurate.
187         if (mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK) {
188             final Task capturedTask = mRecordedWindowContainer.asTask();
189             if (capturedTask.inPinnedWindowingMode()) {
190                 ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
191                         "Content Recording: Display %d was already recording, but "
192                                 + "pause capture since the task is in PIP",
193                         mDisplayContent.getDisplayId());
194                 pauseRecording();
195                 return;
196             }
197         }
198 
199         // Record updated windowing mode, if necessary.
200         int recordedContentWindowingMode = mRecordedWindowContainer.getWindowingMode();
201         if (lastWindowingMode != recordedContentWindowingMode) {
202             mMediaProjectionManager.notifyWindowingModeChanged(
203                     mContentRecordingSession.getContentToRecord(),
204                     mContentRecordingSession.getTargetUid(),
205                     recordedContentWindowingMode
206             );
207         }
208 
209         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
210                 "Content Recording: Display %d was already recording, so apply "
211                         + "transformations if necessary",
212                 mDisplayContent.getDisplayId());
213         // Retrieve the size of the region to record, and continue with the update
214         // if the bounds or orientation has changed.
215         final Rect recordedContentBounds = mRecordedWindowContainer.getBounds();
216         @Configuration.Orientation int recordedContentOrientation =
217                 mRecordedWindowContainer.getConfiguration().orientation;
218         final Point surfaceSize = fetchSurfaceSizeIfPresent();
219         if (!mLastRecordedBounds.equals(recordedContentBounds)
220                 || lastOrientation != recordedContentOrientation
221                 || !mLastConsumingSurfaceSize.equals(surfaceSize)) {
222             if (surfaceSize != null) {
223                 ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
224                         "Content Recording: Going ahead with updating recording for display "
225                                 + "%d to new bounds %s and/or orientation %d and/or surface "
226                                 + "size %s",
227                         mDisplayContent.getDisplayId(), recordedContentBounds,
228                         recordedContentOrientation, surfaceSize);
229                 updateMirroredSurface(mRecordedWindowContainer.getSyncTransaction(),
230                         recordedContentBounds, surfaceSize);
231             } else {
232                 // If the surface removed, do nothing. We will handle this via onDisplayChanged
233                 // (the display will be off if the surface is removed).
234                 ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
235                         "Content Recording: Unable to update recording for display %d to new "
236                                 + "bounds %s and/or orientation %d and/or surface size %s, "
237                                 + "since the surface is not available.",
238                         mDisplayContent.getDisplayId(), recordedContentBounds,
239                         recordedContentOrientation, surfaceSize);
240             }
241         }
242     }
243 
244     /**
245      * Pauses recording on this display content. Note the session does not need to be updated,
246      * since recording can be resumed still.
247      */
pauseRecording()248     void pauseRecording() {
249         if (mRecordedSurface == null) {
250             return;
251         }
252         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
253                 "Content Recording: Display %d has content (%b) so pause recording",
254                 mDisplayContent.getDisplayId(), mDisplayContent.getLastHasContent());
255         // If the display is not on and it is a virtual display, then it no longer has an
256         // associated surface to write output to.
257         // If the display now has content, stop mirroring to it.
258         mDisplayContent.mWmService.mTransactionFactory.get()
259                 // Remove the reference to mMirroredSurface, to clean up associated memory.
260                 .remove(mRecordedSurface)
261                 // Reparent the SurfaceControl of this DisplayContent back to mSurfaceControl,
262                 // to allow content to be added to it. This allows this DisplayContent to stop
263                 // mirroring and show content normally.
264                 .reparent(mDisplayContent.getWindowingLayer(), mDisplayContent.getSurfaceControl())
265                 .reparent(mDisplayContent.getOverlayLayer(), mDisplayContent.getSurfaceControl())
266                 .apply();
267         // Pause mirroring by destroying the reference to the mirrored layer.
268         mRecordedSurface = null;
269         // Do not un-set the token, in case content is removed and recording should begin again.
270     }
271 
272     /**
273      * Stops recording on this DisplayContent, and updates the session details.
274      */
stopRecording()275     void stopRecording() {
276         unregisterListener();
277         if (mRecordedSurface != null) {
278             // Do not wait for the mirrored surface to be garbage collected, but clean up
279             // immediately.
280             mDisplayContent.mWmService.mTransactionFactory.get().remove(mRecordedSurface).apply();
281             mRecordedSurface = null;
282             clearContentRecordingSession();
283             // Do not need to force remove the VirtualDisplay; this is handled by the media
284             // projection service when the display is removed.
285         }
286     }
287 
288 
289     /**
290      * Ensure recording does not fall back to the display stack; ensure the recording is stopped
291      * and the client notified by tearing down the virtual display.
292      */
stopMediaProjection()293     private void stopMediaProjection() {
294         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
295                 "Content Recording: Stop MediaProjection on virtual display %d",
296                 mDisplayContent.getDisplayId());
297         if (mMediaProjectionManager != null) {
298             mMediaProjectionManager.stopActiveProjection();
299         }
300     }
301 
302     /**
303      * Removes both the local cache and WM Service view of the current session, to stop the session
304      * on this display.
305      */
clearContentRecordingSession()306     private void clearContentRecordingSession() {
307         // Update the cached session state first, since updating the service will result in always
308         // returning to this instance to update recording state.
309         mContentRecordingSession = null;
310         mDisplayContent.mWmService.mContentRecordingController.setContentRecordingSessionLocked(
311                 null, mDisplayContent.mWmService);
312     }
313 
unregisterListener()314     private void unregisterListener() {
315         Task recordedTask = mRecordedWindowContainer != null
316                 ? mRecordedWindowContainer.asTask() : null;
317         if (recordedTask == null || !isRecordingContentTask()) {
318             return;
319         }
320         recordedTask.unregisterWindowContainerListener(this);
321         mRecordedWindowContainer = null;
322     }
323 
324     /**
325      * Start recording to this DisplayContent if it does not have its own content. Captures the
326      * content of a WindowContainer indicated by a WindowToken. If unable to start recording, falls
327      * back to original MediaProjection approach.
328      */
startRecordingIfNeeded()329     private void startRecordingIfNeeded() {
330         // Only record if this display does not have its own content, is not recording already,
331         // and if this display is on (it has a surface to write output to).
332         if (mDisplayContent.getLastHasContent() || isCurrentlyRecording()
333                 || mDisplayContent.getDisplayInfo().state == Display.STATE_OFF
334                 || mContentRecordingSession == null) {
335             return;
336         }
337 
338         if (mContentRecordingSession.isWaitingForConsent()) {
339             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do "
340                     + "nothing");
341             return;
342         }
343 
344         mRecordedWindowContainer = retrieveRecordedWindowContainer();
345         if (mRecordedWindowContainer == null) {
346             // Either the token is missing, or the window associated with the token is missing.
347             // Error has already been handled, so just leave.
348             return;
349         }
350 
351         final int contentToRecord = mContentRecordingSession.getContentToRecord();
352 
353         // TODO(b/297514518) Do not start capture if the app is in PIP, the bounds are inaccurate.
354         if (contentToRecord == RECORD_CONTENT_TASK) {
355             if (mRecordedWindowContainer.asTask().inPinnedWindowingMode()) {
356                 ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
357                         "Content Recording: Display %d should start recording, but "
358                                 + "don't yet since the task is in PIP",
359                         mDisplayContent.getDisplayId());
360                 return;
361             }
362         }
363 
364         final Point surfaceSize = fetchSurfaceSizeIfPresent();
365         if (surfaceSize == null) {
366             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
367                     "Content Recording: Unable to start recording for display %d since the "
368                             + "surface is not available.",
369                     mDisplayContent.getDisplayId());
370             return;
371         }
372         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
373                 "Content Recording: Display %d has no content and is on, so start recording for "
374                         + "state %d",
375                 mDisplayContent.getDisplayId(), mDisplayContent.getDisplayInfo().state);
376 
377         // Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture.
378         mRecordedSurface = SurfaceControl.mirrorSurface(
379                 mRecordedWindowContainer.getSurfaceControl());
380         SurfaceControl.Transaction transaction =
381                 mDisplayContent.mWmService.mTransactionFactory.get()
382                         // Set the mMirroredSurface's parent to the root SurfaceControl for this
383                         // DisplayContent. This brings the new mirrored hierarchy under this
384                         // DisplayContent,
385                         // so SurfaceControl will write the layers of this hierarchy to the
386                         // output surface
387                         // provided by the app.
388                         .reparent(mRecordedSurface, mDisplayContent.getSurfaceControl())
389                         // Reparent the SurfaceControl of this DisplayContent to null, to prevent
390                         // content
391                         // being added to it. This ensures that no app launched explicitly on the
392                         // VirtualDisplay will show up as part of the mirrored content.
393                         .reparent(mDisplayContent.getWindowingLayer(), null)
394                         .reparent(mDisplayContent.getOverlayLayer(), null);
395         // Retrieve the size of the DisplayArea to mirror.
396         updateMirroredSurface(transaction, mRecordedWindowContainer.getBounds(), surfaceSize);
397         transaction.apply();
398 
399         // Notify the client about the visibility of the mirrored region, now that we have begun
400         // capture.
401         if (contentToRecord == RECORD_CONTENT_TASK) {
402             mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
403                     mRecordedWindowContainer.asTask().isVisibleRequested());
404         } else {
405             int currentDisplayState =
406                     mRecordedWindowContainer.asDisplayContent().getDisplayInfo().state;
407             mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
408                     currentDisplayState != DISPLAY_STATE_OFF);
409         }
410 
411         // Record initial windowing mode after recording starts.
412         mMediaProjectionManager.notifyWindowingModeChanged(
413                 contentToRecord, mContentRecordingSession.getTargetUid(),
414                 mRecordedWindowContainer.getWindowConfiguration().getWindowingMode());
415 
416         // No need to clean up. In SurfaceFlinger, parents hold references to their children. The
417         // mirrored SurfaceControl is alive since the parent DisplayContent SurfaceControl is
418         // holding a reference to it. Therefore, the mirrored SurfaceControl will be cleaned up
419         // when the VirtualDisplay is destroyed - which will clean up this DisplayContent.
420     }
421 
422     /**
423      * Retrieves the {@link WindowContainer} for the level of the hierarchy to start recording,
424      * indicated by the {@link #mContentRecordingSession}. Performs any error handling and state
425      * updates necessary if the {@link WindowContainer} could not be retrieved.
426      * {@link #mContentRecordingSession} must be non-null.
427      *
428      * @return a {@link WindowContainer} to record, or {@code null} if an error was encountered. The
429      * error is logged and any cleanup is handled.
430      */
431     @Nullable
retrieveRecordedWindowContainer()432     private WindowContainer retrieveRecordedWindowContainer() {
433         @RecordContent final int contentToRecord = mContentRecordingSession.getContentToRecord();
434         final IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord();
435         switch (contentToRecord) {
436             case RECORD_CONTENT_DISPLAY:
437                 // Given the id of the display to record, retrieve the associated DisplayContent.
438                 final DisplayContent dc =
439                         mDisplayContent.mWmService.mRoot.getDisplayContent(
440                                 mContentRecordingSession.getDisplayToRecord());
441                 if (dc == null) {
442                     // Fall back to screenrecording using the data sent to DisplayManager
443                     mDisplayContent.mWmService.mDisplayManagerInternal.setWindowManagerMirroring(
444                             mDisplayContent.getDisplayId(), false);
445                     handleStartRecordingFailed();
446                     ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
447                             "Unable to retrieve window container to start recording for "
448                                     + "display %d", mDisplayContent.getDisplayId());
449                     return null;
450                 }
451                 // TODO(206461622) Migrate to using the RootDisplayArea
452                 return dc;
453             case RECORD_CONTENT_TASK:
454                 // Given the WindowToken of the region to record, retrieve the associated
455                 // SurfaceControl.
456                 if (tokenToRecord == null) {
457                     handleStartRecordingFailed();
458                     ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
459                             "Content Recording: Unable to start recording due to null token for "
460                                     + "display %d",
461                             mDisplayContent.getDisplayId());
462                     return null;
463                 }
464                 Task taskToRecord = WindowContainer.fromBinder(tokenToRecord).asTask();
465                 if (taskToRecord == null) {
466                     handleStartRecordingFailed();
467                     ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
468                             "Content Recording: Unable to retrieve task to start recording for "
469                                     + "display %d",
470                             mDisplayContent.getDisplayId());
471                 } else {
472                     taskToRecord.registerWindowContainerListener(this);
473                 }
474                 return taskToRecord;
475             default:
476                 // Not a valid region, or recording is disabled, so fall back to Display stack
477                 // capture for the entire display.
478                 handleStartRecordingFailed();
479                 ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
480                         "Content Recording: Unable to start recording due to invalid region for "
481                                 + "display %d",
482                         mDisplayContent.getDisplayId());
483                 return null;
484         }
485     }
486 
487     /**
488      * Exit this recording session.
489      * <p>
490      * If this is a task session, stop the recording entirely, including the MediaProjection.
491      * Do not fall back to recording the entire display on the display stack; this would surprise
492      * the user given they selected task capture.
493      * </p><p>
494      * If this is a display session, just stop recording by layer mirroring. Fall back to recording
495      * from the display stack.
496      * </p>
497      */
handleStartRecordingFailed()498     private void handleStartRecordingFailed() {
499         final boolean shouldExitTaskRecording = isRecordingContentTask();
500         unregisterListener();
501         clearContentRecordingSession();
502         if (shouldExitTaskRecording) {
503             // Clean up the cached session first to ensure recording doesn't re-start, since
504             // tearing down the display will generate display events which will trickle back here.
505             stopMediaProjection();
506         }
507     }
508 
computeScaling(int inputSizeX, int inputSizeY, float inputDpiX, float inputDpiY, int outputSizeX, int outputSizeY, float outputDpiX, float outputDpiY, PointF scaleOut)509     private void computeScaling(int inputSizeX, int inputSizeY,
510             float inputDpiX, float inputDpiY,
511             int outputSizeX, int outputSizeY,
512             float outputDpiX, float outputDpiY,
513             PointF scaleOut) {
514         float relAnisotropy = (inputDpiY / inputDpiX) / (outputDpiY / outputDpiX);
515         if (!mCorrectForAnisotropicPixels
516                 || (relAnisotropy > (1 - MAX_ANISOTROPY) && relAnisotropy < (1 + MAX_ANISOTROPY))) {
517             // Calculate the scale to apply to the root mirror SurfaceControl to fit the size of the
518             // output surface.
519             float scaleX = outputSizeX / (float) inputSizeX;
520             float scaleY = outputSizeY / (float) inputSizeY;
521             float scale = Math.min(scaleX, scaleY);
522             scaleOut.x = scale;
523             scaleOut.y = scale;
524             return;
525         }
526 
527         float relDpiX = outputDpiX / inputDpiX;
528         float relDpiY = outputDpiY / inputDpiY;
529 
530         float scale = Math.min(outputSizeX / relDpiX / inputSizeX,
531                 outputSizeY / relDpiY / inputSizeY);
532         scaleOut.x = scale * relDpiX;
533         scaleOut.y = scale * relDpiY;
534     }
535 
536     /**
537      * Apply transformations to the mirrored surface to ensure the captured contents are scaled to
538      * fit and centred in the output surface.
539      *
540      * @param transaction           the transaction to include transformations of mMirroredSurface
541      *                              to. Transaction is not applied before returning.
542      * @param recordedContentBounds bounds of the content to record to the surface provided by
543      *                              the app.
544      * @param surfaceSize           the default size of the surface to write the display area
545      *                              content to
546      */
updateMirroredSurface(SurfaceControl.Transaction transaction, Rect recordedContentBounds, Point surfaceSize)547     @VisibleForTesting void updateMirroredSurface(SurfaceControl.Transaction transaction,
548             Rect recordedContentBounds, Point surfaceSize) {
549 
550         DisplayInfo inputDisplayInfo = mRecordedWindowContainer.mDisplayContent.getDisplayInfo();
551         DisplayInfo outputDisplayInfo = mDisplayContent.getDisplayInfo();
552 
553         PointF scale = new PointF();
554         computeScaling(recordedContentBounds.width(), recordedContentBounds.height(),
555                 inputDisplayInfo.physicalXDpi, inputDisplayInfo.physicalYDpi,
556                 surfaceSize.x, surfaceSize.y,
557                 outputDisplayInfo.physicalXDpi, outputDisplayInfo.physicalYDpi,
558                 scale);
559 
560         int scaledWidth = Math.round(scale.x * (float) recordedContentBounds.width());
561         int scaledHeight = Math.round(scale.y * (float) recordedContentBounds.height());
562 
563         // Calculate the shift to apply to the root mirror SurfaceControl to centre the mirrored
564         // contents in the output surface.
565         int shiftedX = 0;
566         if (scaledWidth != surfaceSize.x) {
567             shiftedX = (surfaceSize.x - scaledWidth) / 2;
568         }
569         int shiftedY = 0;
570         if (scaledHeight != surfaceSize.y) {
571             shiftedY = (surfaceSize.y - scaledHeight) / 2;
572         }
573 
574         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
575                 "Content Recording: Apply transformations of shift %d x %d, scale %f x %f, crop "
576                         + "(aka recorded content size) %d x %d for display %d; display has size "
577                         + "%d x %d; surface has size %d x %d",
578                 shiftedX, shiftedY, scale.x, scale.y, recordedContentBounds.width(),
579                 recordedContentBounds.height(), mDisplayContent.getDisplayId(),
580                 mDisplayContent.getConfiguration().screenWidthDp,
581                 mDisplayContent.getConfiguration().screenHeightDp, surfaceSize.x, surfaceSize.y);
582 
583         transaction
584                 // Crop the area to capture to exclude the 'extra' wallpaper that is used
585                 // for parallax (b/189930234).
586                 .setWindowCrop(mRecordedSurface, recordedContentBounds.width(),
587                         recordedContentBounds.height())
588                 // Scale the root mirror SurfaceControl, based upon the size difference between the
589                 // source (DisplayArea to capture) and output (surface the app reads images from).
590                 .setMatrix(mRecordedSurface, scale.x, 0 /* dtdx */, 0 /* dtdy */, scale.y)
591                 // Position needs to be updated when the mirrored DisplayArea has changed, since
592                 // the content will no longer be centered in the output surface.
593                 .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */);
594         mLastRecordedBounds = new Rect(recordedContentBounds);
595         mLastConsumingSurfaceSize.x = surfaceSize.x;
596         mLastConsumingSurfaceSize.y = surfaceSize.y;
597         // Request to notify the client about the resize.
598         mMediaProjectionManager.notifyActiveProjectionCapturedContentResized(
599                 mLastRecordedBounds.width(), mLastRecordedBounds.height());
600     }
601 
602     /**
603      * Returns a non-null {@link Point} if the surface is present, or null otherwise
604      */
605     @Nullable
fetchSurfaceSizeIfPresent()606     private Point fetchSurfaceSizeIfPresent() {
607         // Retrieve the default size of the surface the app provided to
608         // MediaProjection#createVirtualDisplay. Note the app is the consumer of the surface,
609         // since it reads out buffers from the surface, and SurfaceFlinger is the producer since
610         // it writes the mirrored layers to the buffers.
611         Point surfaceSize =
612                 mDisplayContent.mWmService.mDisplayManagerInternal.getDisplaySurfaceDefaultSize(
613                         mDisplayContent.getDisplayId());
614         if (surfaceSize == null) {
615             // Layer mirroring started with a null surface, so do not apply any transformations yet.
616             // State of virtual display will change to 'ON' when the surface is set.
617             // will get event DISPLAY_DEVICE_EVENT_CHANGED
618             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
619                     "Content Recording: Provided surface for recording on display %d is not "
620                             + "present, so do not update the surface",
621                     mDisplayContent.getDisplayId());
622             return null;
623         }
624         return surfaceSize;
625     }
626 
627     // WindowContainerListener
628     @Override
onRemoved()629     public void onRemoved() {
630         ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
631                 "Content Recording: Recorded task is removed, so stop recording on display %d",
632                 mDisplayContent.getDisplayId());
633 
634         unregisterListener();
635         // Stop mirroring and teardown.
636         clearContentRecordingSession();
637         // Clean up the cached session first to ensure recording doesn't re-start, since
638         // tearing down the display will generate display events which will trickle back here.
639         stopMediaProjection();
640     }
641 
642     // WindowContainerListener
643     @Override
onMergedOverrideConfigurationChanged( Configuration mergedOverrideConfiguration)644     public void onMergedOverrideConfigurationChanged(
645             Configuration mergedOverrideConfiguration) {
646         WindowContainerListener.super.onMergedOverrideConfigurationChanged(
647                 mergedOverrideConfiguration);
648         onConfigurationChanged(mLastOrientation, mLastWindowingMode);
649         mLastOrientation = mergedOverrideConfiguration.orientation;
650         mLastWindowingMode = mergedOverrideConfiguration.windowConfiguration.getWindowingMode();
651     }
652 
653     // WindowContainerListener
654     @Override
onVisibleRequestedChanged(boolean isVisibleRequested)655     public void onVisibleRequestedChanged(boolean isVisibleRequested) {
656         // Check still recording just to be safe.
657         if (isCurrentlyRecording() && mLastRecordedBounds != null) {
658             mMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
659                     isVisibleRequested);
660 
661             if (mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK) {
662                 // If capturing a task, then the toggle visibility of the recorded surface to match
663                 // visibility of the task, so we don't capture any mid-transition frames
664                 mRecordedWindowContainer.getSyncTransaction()
665                         .setVisibility(mRecordedSurface, isVisibleRequested);
666                 mRecordedWindowContainer.scheduleAnimation();
667             }
668         }
669     }
670 
671     @VisibleForTesting interface MediaProjectionManagerWrapper {
stopActiveProjection()672         void stopActiveProjection();
notifyActiveProjectionCapturedContentResized(int width, int height)673         void notifyActiveProjectionCapturedContentResized(int width, int height);
notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible)674         void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible);
notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode)675         void notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode);
676     }
677 
678     private static final class RemoteMediaProjectionManagerWrapper implements
679             MediaProjectionManagerWrapper {
680 
681         private final int mDisplayId;
682         @Nullable private IMediaProjectionManager mIMediaProjectionManager = null;
683 
RemoteMediaProjectionManagerWrapper(int displayId)684         RemoteMediaProjectionManagerWrapper(int displayId) {
685             mDisplayId = displayId;
686         }
687 
688         @Override
stopActiveProjection()689         public void stopActiveProjection() {
690             fetchMediaProjectionManager();
691             if (mIMediaProjectionManager == null) {
692                 return;
693             }
694             try {
695                 ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
696                         "Content Recording: stopping active projection for display %d",
697                         mDisplayId);
698                 mIMediaProjectionManager.stopActiveProjection();
699             } catch (RemoteException e) {
700                 ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
701                         "Content Recording: Unable to tell MediaProjectionManagerService to stop "
702                                 + "the active projection for display %d: %s",
703                         mDisplayId, e);
704             }
705         }
706 
707         @Override
notifyActiveProjectionCapturedContentResized(int width, int height)708         public void notifyActiveProjectionCapturedContentResized(int width, int height) {
709             fetchMediaProjectionManager();
710             if (mIMediaProjectionManager == null) {
711                 return;
712             }
713             try {
714                 mIMediaProjectionManager.notifyActiveProjectionCapturedContentResized(width,
715                         height);
716             } catch (RemoteException e) {
717                 ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
718                         "Content Recording: Unable to tell MediaProjectionManagerService about "
719                                 + "resizing the active projection: %s",
720                         e);
721             }
722         }
723 
724         @Override
notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible)725         public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) {
726             fetchMediaProjectionManager();
727             if (mIMediaProjectionManager == null) {
728                 return;
729             }
730             try {
731                 mIMediaProjectionManager.notifyActiveProjectionCapturedContentVisibilityChanged(
732                         isVisible);
733             } catch (RemoteException e) {
734                 ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
735                         "Content Recording: Unable to tell MediaProjectionManagerService about "
736                                 + "visibility change on the active projection: %s",
737                         e);
738             }
739         }
740 
741         @Override
notifyWindowingModeChanged(int contentToRecord, int targetUid, int windowingMode)742         public void notifyWindowingModeChanged(int contentToRecord, int targetUid,
743                 int windowingMode) {
744             fetchMediaProjectionManager();
745             if (mIMediaProjectionManager == null) {
746                 return;
747             }
748             try {
749                 mIMediaProjectionManager.notifyWindowingModeChanged(
750                         contentToRecord, targetUid, windowingMode);
751             } catch (RemoteException e) {
752                 ProtoLog.e(WM_DEBUG_CONTENT_RECORDING,
753                         "Content Recording: Unable to tell log windowing mode change: %s", e);
754             }
755         }
756 
fetchMediaProjectionManager()757         private void fetchMediaProjectionManager() {
758             if (mIMediaProjectionManager != null) {
759                 return;
760             }
761             IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
762             if (b == null) {
763                 return;
764             }
765             mIMediaProjectionManager = IMediaProjectionManager.Stub.asInterface(b);
766         }
767     }
768 
isRecordingContentTask()769     private boolean isRecordingContentTask() {
770         return mContentRecordingSession != null
771                 && mContentRecordingSession.getContentToRecord() == RECORD_CONTENT_TASK;
772     }
773 }
774