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