Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c0d9771e authored by Mark Renouf's avatar Mark Renouf Committed by Automerger Merge Worker
Browse files

Merge "Adds test coverage for TakeScreenshotService" into tm-qpr-dev am: b53e6934

parents 60fe7b36 b53e6934
Loading
Loading
Loading
Loading
+24 −22
Original line number Original line Diff line number Diff line
@@ -53,8 +53,7 @@ import android.util.Log;
import android.view.WindowManager;
import android.view.WindowManager;
import android.widget.Toast;
import android.widget.Toast;


import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotHelper;
import com.android.systemui.R;
import com.android.systemui.R;
@@ -137,7 +136,7 @@ public class TakeScreenshotService extends Service {
    }
    }


    @Override
    @Override
    public IBinder onBind(@NonNull Intent intent) {
    public IBinder onBind(Intent intent) {
        registerReceiver(mCloseSystemDialogs, new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS),
        registerReceiver(mCloseSystemDialogs, new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS),
                Context.RECEIVER_EXPORTED);
                Context.RECEIVER_EXPORTED);
        final Messenger m = new Messenger(mHandler);
        final Messenger m = new Messenger(mHandler);
@@ -184,13 +183,23 @@ public class TakeScreenshotService extends Service {
        }
        }
    }
    }


    /** Respond to incoming Message via Binder (Messenger) */
    @MainThread
    @MainThread
    private boolean handleMessage(Message msg) {
    private boolean handleMessage(Message msg) {
        final Messenger replyTo = msg.replyTo;
        final Messenger replyTo = msg.replyTo;
        final Consumer<Uri> uriConsumer = (uri) -> reportUri(replyTo, uri);
        final Consumer<Uri> onSaved = (uri) -> reportUri(replyTo, uri);
        RequestCallback requestCallback = new RequestCallbackImpl(replyTo);
        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<Uri> onSaved,
            RequestCallback callback) {
        // If the storage for this user is locked, we have no place to store
        // 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
        // the screenshot, so skip taking it instead of showing a misleading
        // animation and error notification.
        // animation and error notification.
@@ -198,8 +207,8 @@ public class TakeScreenshotService extends Service {
            Log.w(TAG, "Skipping screenshot because storage is locked!");
            Log.w(TAG, "Skipping screenshot because storage is locked!");
            mNotificationsController.notifyScreenshotError(
            mNotificationsController.notifyScreenshotError(
                    R.string.screenshot_failed_to_save_user_locked_text);
                    R.string.screenshot_failed_to_save_user_locked_text);
            requestCallback.reportError();
            callback.reportError();
            return true;
            return;
        }
        }


        if (mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) {
        if (mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) {
@@ -211,33 +220,26 @@ public class TakeScreenshotService extends Service {
                        () -> mContext.getString(R.string.screenshot_blocked_by_admin));
                        () -> mContext.getString(R.string.screenshot_blocked_by_admin));
                mHandler.post(() ->
                mHandler.post(() ->
                        Toast.makeText(mContext, blockedByAdminText, Toast.LENGTH_SHORT).show());
                        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)) {
        if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) {
            Log.d(TAG, "handleMessage: Using request processor");
            Log.d(TAG, "handleMessage: Using request processor");
            mProcessor.processAsync(screenshotRequest,
            mProcessor.processAsync(request,
                    (request) -> dispatchToController(request, uriConsumer, requestCallback));
                    (r) -> dispatchToController(r, onSaved, callback));
            return true;
        }
        }


        dispatchToController(screenshotRequest, uriConsumer, requestCallback);
        dispatchToController(request, onSaved, callback);
        return true;
    }
    }


    private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
    private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
            Consumer<Uri> uriConsumer, RequestCallback callback) {
            Consumer<Uri> uriConsumer, RequestCallback callback) {


        ComponentName topComponent = request.getTopComponent();
        ComponentName topComponent = request.getTopComponent();
        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(request.getSource()), 0,
                topComponent == null ? "" : topComponent.getPackageName());


        switch (request.getType()) {
        switch (request.getType()) {
            case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
            case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
+237 −0
Original line number Original line Diff line number Diff line
/*
 * 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<Application>()
    private val controller = mock<ScreenshotController>()
    private val userManager = mock<UserManager>()
    private val requestProcessor = mock<RequestProcessor>()
    private val devicePolicyManager = mock<DevicePolicyManager>()
    private val devicePolicyResourcesManager = mock<DevicePolicyResourcesManager>()
    private val notificationsController = mock<ScreenshotNotificationsController>()
    private val callback = mock<RequestCallback>()

    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<String> */ 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))!!
}