From d4e5ef1b121157f52db3de0ddb31d0a96df2075d Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Mon, 22 Aug 2022 19:57:34 -0400 Subject: [PATCH] Adds test coverage for TakeScreenshotService Test: atest TakeScreenshotServiceTest Bug: 159422805 Change-Id: I2dce0852c7f2ab178faa09c69e2bc7810a0a9098 --- .../screenshot/TakeScreenshotService.java | 46 ++-- .../screenshot/TakeScreenshotServiceTest.kt | 237 ++++++++++++++++++ 2 files changed, 261 insertions(+), 22 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 8b37aab876659..35f32caffe210 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -53,8 +53,7 @@ import android.util.Log; import android.view.WindowManager; import android.widget.Toast; -import androidx.annotation.NonNull; - +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.R; @@ -137,7 +136,7 @@ public class TakeScreenshotService extends Service { } @Override - public IBinder onBind(@NonNull Intent intent) { + public IBinder onBind(Intent intent) { registerReceiver(mCloseSystemDialogs, new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS), Context.RECEIVER_EXPORTED); final Messenger m = new Messenger(mHandler); @@ -184,13 +183,23 @@ public class TakeScreenshotService extends Service { } } - /** Respond to incoming Message via Binder (Messenger) */ @MainThread private boolean handleMessage(Message msg) { final Messenger replyTo = msg.replyTo; - final Consumer uriConsumer = (uri) -> reportUri(replyTo, uri); - RequestCallback requestCallback = new RequestCallbackImpl(replyTo); + final Consumer onSaved = (uri) -> reportUri(replyTo, uri); + RequestCallback callback = new RequestCallbackImpl(replyTo); + ScreenshotHelper.ScreenshotRequest request = + (ScreenshotHelper.ScreenshotRequest) msg.obj; + + handleRequest(request, onSaved, callback); + return true; + } + + @MainThread + @VisibleForTesting + void handleRequest(ScreenshotHelper.ScreenshotRequest request, Consumer onSaved, + RequestCallback callback) { // If the storage for this user is locked, we have no place to store // the screenshot, so skip taking it instead of showing a misleading // animation and error notification. @@ -198,8 +207,8 @@ public class TakeScreenshotService extends Service { Log.w(TAG, "Skipping screenshot because storage is locked!"); mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_save_user_locked_text); - requestCallback.reportError(); - return true; + callback.reportError(); + return; } if (mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) { @@ -211,33 +220,26 @@ public class TakeScreenshotService extends Service { () -> mContext.getString(R.string.screenshot_blocked_by_admin)); mHandler.post(() -> Toast.makeText(mContext, blockedByAdminText, Toast.LENGTH_SHORT).show()); - requestCallback.reportError(); + callback.reportError(); }); - return true; + return; } - ScreenshotHelper.ScreenshotRequest screenshotRequest = - (ScreenshotHelper.ScreenshotRequest) msg.obj; - - ComponentName topComponent = screenshotRequest.getTopComponent(); - mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()), 0, - topComponent == null ? "" : topComponent.getPackageName()); - if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) { Log.d(TAG, "handleMessage: Using request processor"); - mProcessor.processAsync(screenshotRequest, - (request) -> dispatchToController(request, uriConsumer, requestCallback)); - return true; + mProcessor.processAsync(request, + (r) -> dispatchToController(r, onSaved, callback)); } - dispatchToController(screenshotRequest, uriConsumer, requestCallback); - return true; + dispatchToController(request, onSaved, callback); } private void dispatchToController(ScreenshotHelper.ScreenshotRequest request, Consumer uriConsumer, RequestCallback callback) { ComponentName topComponent = request.getTopComponent(); + mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(request.getSource()), 0, + topComponent == null ? "" : topComponent.getPackageName()); switch (request.getType()) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt new file mode 100644 index 0000000000000..83e56daf1fbc3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt @@ -0,0 +1,237 @@ +/* + * 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.Application +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources.Strings.SystemUi.SCREENSHOT_BLOCKED_BY_ADMIN +import android.app.admin.DevicePolicyResourcesManager +import android.content.ComponentName +import android.graphics.Bitmap +import android.graphics.Bitmap.Config.HARDWARE +import android.graphics.ColorSpace +import android.graphics.Insets +import android.graphics.Rect +import android.hardware.HardwareBuffer +import android.os.UserHandle +import android.os.UserManager +import android.testing.AndroidTestingRunner +import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD +import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW +import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN +import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE +import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.util.ScreenshotHelper +import com.android.internal.util.ScreenshotHelper.ScreenshotRequest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW +import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argThat +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.isNull +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` as whenever + +private const val USER_ID = 1 +private const val TASK_ID = 1 + +@RunWith(AndroidTestingRunner::class) +class TakeScreenshotServiceTest : SysuiTestCase() { + + private val application = mock() + private val controller = mock() + private val userManager = mock() + private val requestProcessor = mock() + private val devicePolicyManager = mock() + private val devicePolicyResourcesManager = mock() + private val notificationsController = mock() + private val callback = mock() + + private val eventLogger = UiEventLoggerFake() + private val flags = FakeFeatureFlags() + private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java) + + private val service = TakeScreenshotService( + controller, userManager, devicePolicyManager, eventLogger, + notificationsController, mContext, Runnable::run, flags, requestProcessor) + + @Before + fun setUp() { + whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager) + whenever(devicePolicyManager.getScreenCaptureDisabled( + /* admin component (null: any admin) */ isNull(), eq(UserHandle.USER_ALL))) + .thenReturn(false) + whenever(userManager.isUserUnlocked).thenReturn(true) + + flags.set(SCREENSHOT_REQUEST_PROCESSOR, false) + + service.attach( + mContext, + /* thread = */ null, + /* className = */ null, + /* token = */ null, + application, + /* activityManager = */ null) + } + + @Test + fun testServiceLifecycle() { + service.onCreate() + service.onBind(null /* unused: Intent */) + + service.onUnbind(null /* unused: Intent */) + verify(controller).removeWindow() + + service.onDestroy() + verify(controller).onDestroy() + } + + @Test + fun takeScreenshotFullscreen() { + val request = ScreenshotRequest( + TAKE_SCREENSHOT_FULLSCREEN, + SCREENSHOT_KEY_CHORD, + topComponent) + + service.handleRequest(request, { /* onSaved */ }, callback) + + verify(controller).takeScreenshotFullscreen( + eq(topComponent), + /* onSavedListener = */ any(), + /* requestCallback = */ any()) + + assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1) + val logEvent = eventLogger.get(0) + + assertEquals("Expected SCREENSHOT_REQUESTED UiEvent", + logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id) + assertEquals("Expected supplied package name", + topComponent.packageName, eventLogger.get(0).packageName) + } + + @Test + fun takeScreenshotPartial() { + val request = ScreenshotRequest( + TAKE_SCREENSHOT_SELECTED_REGION, + SCREENSHOT_KEY_CHORD, + /* topComponent = */ null) + + service.handleRequest(request, { /* onSaved */ }, callback) + + verify(controller).takeScreenshotPartial( + /* topComponent = */ isNull(), + /* onSavedListener = */ any(), + /* requestCallback = */ any()) + + assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1) + val logEvent = eventLogger.get(0) + + assertEquals("Expected SCREENSHOT_REQUESTED UiEvent", + logEvent.eventId, SCREENSHOT_REQUESTED_KEY_CHORD.id) + assertEquals("Expected empty package name in UiEvent", "", eventLogger.get(0).packageName) + } + + @Test + fun takeScreenshotProvidedImage() { + val bounds = Rect(50, 50, 150, 150) + val bitmap = makeHardwareBitmap(100, 100) + val bitmapBundle = ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(bitmap) + + val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW, + bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, topComponent) + + service.handleRequest(request, { /* onSaved */ }, callback) + + verify(controller).handleImageAsScreenshot( + argThat { b -> b.equalsHardwareBitmap(bitmap) }, + eq(bounds), + eq(Insets.NONE), eq(TASK_ID), eq(USER_ID), eq(topComponent), + /* onSavedListener = */ any(), /* requestCallback = */ any()) + + assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1) + val logEvent = eventLogger.get(0) + + assertEquals("Expected SCREENSHOT_REQUESTED_* UiEvent", + logEvent.eventId, SCREENSHOT_REQUESTED_OVERVIEW.id) + assertEquals("Expected supplied package name", + topComponent.packageName, eventLogger.get(0).packageName) + } + + @Test + fun takeScreenshotFullscreen_userLocked() { + whenever(userManager.isUserUnlocked).thenReturn(false) + + val request = ScreenshotRequest( + TAKE_SCREENSHOT_FULLSCREEN, + SCREENSHOT_KEY_CHORD, + topComponent) + + service.handleRequest(request, { /* onSaved */ }, callback) + + verify(notificationsController).notifyScreenshotError(anyInt()) + verify(callback).reportError() + verifyZeroInteractions(controller) + } + + @Test + fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() { + whenever(devicePolicyManager.getScreenCaptureDisabled( + isNull(), eq(UserHandle.USER_ALL)) + ).thenReturn(true) + + whenever(devicePolicyResourcesManager.getString( + eq(SCREENSHOT_BLOCKED_BY_ADMIN), + /* Supplier */ any(), + )).thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN") + + val request = ScreenshotRequest( + TAKE_SCREENSHOT_FULLSCREEN, + SCREENSHOT_KEY_CHORD, + topComponent) + + service.handleRequest(request, { /* onSaved */ }, callback) + + // error shown: Toast.makeText(...).show(), untestable + verify(callback).reportError() + verifyZeroInteractions(controller) + } +} + +private fun Bitmap.equalsHardwareBitmap(other: Bitmap): Boolean { + return config == HARDWARE && + other.config == HARDWARE && + hardwareBuffer == other.hardwareBuffer && + colorSpace == other.colorSpace +} + +/** A hardware Bitmap is mandated by use of ScreenshotHelper.HardwareBitmapBundler */ +private fun makeHardwareBitmap(width: Int, height: Int): Bitmap { + val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1, + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE) + return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!! +}