Extracts image acquisition to a separate class

This is moved to a separate class for to avoid code duplication
and improve testability by providing the ability to unit test
in isolation from SurfaceFlinger and without an active physical
display.

Bug: 231957192
Test: atest ImageCaptureImplTest
Change-Id: Ie8d9dc1a2325856c09ab023099488f799d065a19
This commit is contained in:
Mark Renouf
2022-07-25 15:45:11 -04:00
parent d1a0f4b13e
commit afbac47080
5 changed files with 197 additions and 31 deletions

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenshot
import android.graphics.Bitmap
import android.graphics.Rect
interface ImageCapture {
fun captureDisplay(displayId: Int, crop: Rect? = null): Bitmap?
fun captureTask(taskId: Int): Bitmap?
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenshot
import android.app.IActivityTaskManager
import android.graphics.Bitmap
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.os.IBinder
import android.util.Log
import android.view.DisplayAddress
import android.view.SurfaceControl
import android.view.SurfaceControl.DisplayCaptureArgs
import android.view.SurfaceControl.ScreenshotHardwareBuffer
import androidx.annotation.VisibleForTesting
import javax.inject.Inject
private const val TAG = "ImageCaptureImpl"
open class ImageCaptureImpl @Inject constructor(
private val displayManager: DisplayManager,
private val atmService: IActivityTaskManager
) : ImageCapture {
override fun captureDisplay(displayId: Int, crop: Rect?): Bitmap? {
val width = crop?.width() ?: 0
val height = crop?.height() ?: 0
val sourceCrop = crop ?: Rect()
val displayToken = physicalDisplayToken(displayId) ?: return null
val buffer = captureDisplay(displayToken, width, height, sourceCrop)
return buffer?.asBitmap()
}
override fun captureTask(taskId: Int): Bitmap? {
val snapshot = atmService.takeTaskSnapshot(taskId)
return Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace)
}
@VisibleForTesting
open fun physicalDisplayToken(displayId: Int): IBinder? {
val display = displayManager.getDisplay(displayId)
if (display == null) {
Log.e(TAG, "No display with id: $displayId")
return null
}
val address = display.address
if (address !is DisplayAddress.Physical) {
Log.e(TAG, "Display does not have a physical address: $display")
return null
}
return SurfaceControl.getPhysicalDisplayToken(address.physicalDisplayId)
}
@VisibleForTesting
open fun captureDisplay(displayToken: IBinder, width: Int, height: Int, crop: Rect): ScreenshotHardwareBuffer? {
val captureArgs = DisplayCaptureArgs.Builder(displayToken)
.setSize(width, height)
.setSourceCrop(crop)
.build()
return SurfaceControl.captureDisplay(captureArgs)
}
}

View File

@@ -57,14 +57,12 @@ import android.media.AudioSystem;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.Display;
import android.view.DisplayAddress;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.KeyEvent;
@@ -72,7 +70,6 @@ import android.view.LayoutInflater;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.ScrollCaptureResponse;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
@@ -249,6 +246,7 @@ public class ScreenshotController {
private final ScreenshotSmartActions mScreenshotSmartActions;
private final UiEventLogger mUiEventLogger;
private final ImageExporter mImageExporter;
private final ImageCapture mImageCapture;
private final Executor mMainExecutor;
private final ExecutorService mBgExecutor;
private final BroadcastSender mBroadcastSender;
@@ -295,6 +293,7 @@ public class ScreenshotController {
ScrollCaptureClient scrollCaptureClient,
UiEventLogger uiEventLogger,
ImageExporter imageExporter,
ImageCapture imageCapture,
@Main Executor mainExecutor,
ScrollCaptureController scrollCaptureController,
LongScreenshotData longScreenshotHolder,
@@ -308,6 +307,7 @@ public class ScreenshotController {
mScrollCaptureClient = scrollCaptureClient;
mUiEventLogger = uiEventLogger;
mImageExporter = imageExporter;
mImageCapture = imageCapture;
mMainExecutor = mainExecutor;
mScrollCaptureController = scrollCaptureController;
mLongScreenshotHolder = longScreenshotHolder;
@@ -531,7 +531,7 @@ public class ScreenshotController {
// copy the input Rect, since SurfaceControl.screenshot can mutate it
Rect screenRect = new Rect(crop);
Bitmap screenshot = captureScreenshot(crop);
Bitmap screenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY, crop);
if (screenshot == null) {
Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
@@ -549,32 +549,6 @@ public class ScreenshotController {
ClipboardOverlayController.SELF_PERMISSION);
}
private Bitmap captureScreenshot(Rect crop) {
int width = crop.width();
int height = crop.height();
Bitmap screenshot = null;
final Display display = getDefaultDisplay();
final DisplayAddress address = display.getAddress();
if (!(address instanceof DisplayAddress.Physical)) {
Log.e(TAG, "Skipping Screenshot - Default display does not have a physical address: "
+ display);
} else {
final DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) address;
final IBinder displayToken = SurfaceControl.getPhysicalDisplayToken(
physicalAddress.getPhysicalDisplayId());
final SurfaceControl.DisplayCaptureArgs captureArgs =
new SurfaceControl.DisplayCaptureArgs.Builder(displayToken)
.setSourceCrop(crop)
.setSize(width, height)
.build();
final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
SurfaceControl.captureDisplay(captureArgs);
screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap();
}
return screenshot;
}
private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
Insets screenInsets, ComponentName topComponent, boolean showFlash) {
withWindowAttached(() ->
@@ -720,7 +694,7 @@ public class ScreenshotController {
mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
DisplayMetrics displayMetrics = new DisplayMetrics();
getDefaultDisplay().getRealMetrics(displayMetrics);
Bitmap newScreenshot = captureScreenshot(
Bitmap newScreenshot = mImageCapture.captureDisplay(DEFAULT_DISPLAY,
new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,

View File

@@ -18,6 +18,8 @@ package com.android.systemui.screenshot.dagger;
import android.app.Service;
import com.android.systemui.screenshot.ImageCapture;
import com.android.systemui.screenshot.ImageCaptureImpl;
import com.android.systemui.screenshot.TakeScreenshotService;
import dagger.Binds;
@@ -37,4 +39,6 @@ public abstract class ScreenshotModule {
@ClassKey(TakeScreenshotService.class)
public abstract Service bindTakeScreenshotService(TakeScreenshotService service);
@Binds
public abstract ImageCapture bindImageCapture(ImageCaptureImpl capture);
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.screenshot
import android.app.IActivityTaskManager
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.os.Binder
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.SurfaceControl.ScreenshotHardwareBuffer
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
/**
* Test the logic within ImageCaptureImpl
*/
@RunWith(AndroidTestingRunner::class)
class ImageCaptureImplTest : SysuiTestCase() {
private val displayManager = mock<DisplayManager>()
private val atmService = mock<IActivityTaskManager>()
private val capture = TestableImageCaptureImpl(displayManager, atmService)
@Test
fun captureDisplayWithCrop() {
capture.captureDisplay(Display.DEFAULT_DISPLAY, Rect(1, 2, 3, 4))
assertThat(capture.token).isNotNull()
assertThat(capture.width!!).isEqualTo(2)
assertThat(capture.height!!).isEqualTo(2)
assertThat(capture.crop!!).isEqualTo(Rect(1, 2, 3, 4))
}
@Test
fun captureDisplayWithNullCrop() {
capture.captureDisplay(Display.DEFAULT_DISPLAY, null)
assertThat(capture.token).isNotNull()
assertThat(capture.width!!).isEqualTo(0)
assertThat(capture.height!!).isEqualTo(0)
assertThat(capture.crop!!).isEqualTo(Rect())
}
class TestableImageCaptureImpl(
displayManager: DisplayManager,
atmService: IActivityTaskManager
) :
ImageCaptureImpl(displayManager, atmService) {
var token: IBinder? = null
var width: Int? = null
var height: Int? = null
var crop: Rect? = null
override fun physicalDisplayToken(displayId: Int): IBinder {
return Binder()
}
override fun captureDisplay(displayToken: IBinder, width: Int, height: Int, crop: Rect):
ScreenshotHardwareBuffer {
this.token = displayToken
this.width = width
this.height = height
this.crop = crop
return ScreenshotHardwareBuffer(null, null, false, false)
}
}
}