chromium/chrome/android/javatests/src/org/chromium/chrome/browser/screenshot_monitor/ScreenshotMonitorTest.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.screenshot_monitor;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;

import androidx.core.content.ContextCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.display.DisplayAndroid;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/** Tests ScreenshotMonitor. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
public class ScreenshotMonitorTest {
    private static final String TAG = "ScreenshotTest";
    private static final Uri TEST_URI = Uri.parse("content://media/external/images/media/101");

    private ScreenshotMonitorImpl mTestScreenshotMonitor;
    private TestScreenshotMonitorDelegate mTestScreenshotMonitorDelegate;
    private ContentObserver mContentObserver;

    private MockContentResolver mMockContentResolver = new MockContentResolver();

    @Mock private DisplayAndroid mDisplayAndroid;

    static class TestScreenshotMonitorDelegate implements ScreenshotMonitorDelegate {
        // This is modified on the UI thread and accessed on the test thread.
        public final AtomicInteger screenshotShowUiCount = new AtomicInteger();

        @Override
        public void onScreenshotTaken() {
            Assert.assertTrue(ThreadUtils.runningOnUiThread());
            screenshotShowUiCount.getAndIncrement();
        }
    }

    private static class TestContext extends ContextWrapper {
        public TestContext(Context base) {
            super(base);
        }

        @Override
        public int checkPermission(String permission, int pid, int uid) {
            return PackageManager.PERMISSION_GRANTED;
        }
    }

    @Before
    public void setUp() {
        // Replaces the application context with a test implementation which will return true for
        // permission requests. This is needed for the permission check in
        // ScreenshotMonitorImpl#doesChangeLookLikeScreenshot.
        Context context = new TestContext(ApplicationProvider.getApplicationContext());
        ContextUtils.initApplicationContextForTests(context);
        Assume.assumeTrue(
                ContextCompat.checkSelfPermission(
                                ContextUtils.getApplicationContext(),
                                MimeTypeUtils.getPermissionNameForMimeType(
                                        MimeTypeUtils.Type.IMAGE))
                        == PackageManager.PERMISSION_GRANTED);

        MockitoAnnotations.initMocks(this);
        mTestScreenshotMonitorDelegate = new TestScreenshotMonitorDelegate();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTestScreenshotMonitor =
                            new ScreenshotMonitorImpl(
                                    mTestScreenshotMonitorDelegate,
                                    null,
                                    mMockContentResolver,
                                    mDisplayAndroid);
                    mContentObserver = mTestScreenshotMonitor.getContentObserver();
                });
    }

    private void mockValidContentResolver(String path, String width, String height) {
        final Cursor cursor = Mockito.mock(Cursor.class);
        Mockito.doReturn(true).when(cursor).moveToNext();

        Mockito.doReturn(1)
                .when(cursor)
                .getColumnIndexOrThrow(Mockito.eq(MediaStore.MediaColumns.DATA));
        Mockito.doReturn(2)
                .when(cursor)
                .getColumnIndexOrThrow(Mockito.eq(MediaStore.MediaColumns.WIDTH));
        Mockito.doReturn(3)
                .when(cursor)
                .getColumnIndexOrThrow(Mockito.eq(MediaStore.MediaColumns.HEIGHT));
        Mockito.doReturn(path).when(cursor).getString(Mockito.eq(1));
        Mockito.doReturn(width).when(cursor).getString(Mockito.eq(2));
        Mockito.doReturn(height).when(cursor).getString(Mockito.eq(3));

        mMockContentResolver.addProvider(
                "media",
                new MockContentProvider() {
                    @Override
                    public Cursor query(
                            Uri uri,
                            String[] projection,
                            String selection,
                            String[] selectionArgs,
                            String sortOrder) {
                        return cursor;
                    }
                });
    }

    private void mockDisplay(int width, int height) {
        Mockito.doReturn(width).when(mDisplayAndroid).getDisplayWidth();
        Mockito.doReturn(height).when(mDisplayAndroid).getDisplayHeight();
    }

    private void mockValidScreenshot() {}

    /**
     * Verify that if monitoring starts, the delegate should be called. Also verify that the inner
     * TestFileObserver monitors as expected.
     */
    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testDelegateCalledOnEvent() {
        mockDisplay(50, 100);
        mockValidContentResolver("Screenshot", "50", "100");

        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(1);

        stopMonitoringOnUiThreadBlocking();
    }

    /** Verify that the delegate is called after a restart. */
    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testRestartShouldTriggerDelegate() {
        mockDisplay(50, 100);
        mockValidContentResolver("Screenshot", "50", "100");

        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(1);

        stopMonitoringOnUiThreadBlocking();

        // Restart and call onEvent a second time
        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(1, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(2);
    }

    /** Verify that if monitoring stops, the delegate should not be called. */
    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testStopMonitoringShouldNotTriggerDelegate() {
        mockDisplay(50, 100);
        mockValidContentResolver("Screenshot", "50", "100");

        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        stopMonitoringOnUiThreadBlocking();

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(0);
    }

    /** Verify that if monitoring is never started, the delegate should not be called. */
    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testNoMonitoringShouldNotTriggerDelegate() {
        mockDisplay(50, 100);
        mockValidContentResolver("Screenshot", "50", "100");

        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(0);
    }

    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testRotatedContent() {
        mockDisplay(100, 50);
        mockValidContentResolver("Screenshot", "50", "100");

        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(1);
    }

    @Test
    @SmallTest
    @Feature({"FeatureEngagement", "Screenshot"})
    public void testInvalidSize() {
        mockDisplay(150, 150);
        mockValidContentResolver("Screenshot", "50", "100");

        startMonitoringOnUiThreadBlocking();
        Assert.assertEquals(0, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());

        mContentObserver.onChange(true, TEST_URI);
        assertScreenshotShowUiCountOnUiThreadBlocking(0);
    }

    // This ensures that the UI thread finishes executing startMonitoring.
    private void startMonitoringOnUiThreadBlocking() {
        final Semaphore semaphore = new Semaphore(0);

        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                new Runnable() {
                    @Override
                    public void run() {
                        mTestScreenshotMonitor.startMonitoring();
                        semaphore.release();
                    }
                });
        try {
            Assert.assertTrue(semaphore.tryAcquire(10, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            Log.e(TAG, "Cannot acquire semaphore");
        }
    }

    // This ensures that the UI thread finishes executing stopMonitoring.
    private void stopMonitoringOnUiThreadBlocking() {
        final Semaphore semaphore = new Semaphore(0);

        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                new Runnable() {
                    @Override
                    public void run() {
                        mTestScreenshotMonitor.stopMonitoring();
                        semaphore.release();
                    }
                });
        try {
            Assert.assertTrue(semaphore.tryAcquire(10, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            Log.e(TAG, "Cannot acquire semaphore");
        }
    }

    // This ensures that after UI thread finishes all tasks, screenshotShowUiCount equals
    // expectedCount.
    private void assertScreenshotShowUiCountOnUiThreadBlocking(int expectedCount) {
        final Semaphore semaphore = new Semaphore(0);

        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                new Runnable() {
                    @Override
                    public void run() {
                        semaphore.release();
                    }
                });
        try {
            Assert.assertTrue(semaphore.tryAcquire(10, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            Log.e(TAG, "Cannot acquire semaphore");
        }
        Assert.assertEquals(
                expectedCount, mTestScreenshotMonitorDelegate.screenshotShowUiCount.get());
    }
}