chromium/chrome/android/junit/src/org/chromium/chrome/browser/OmahaServiceStartDelayerTest.java

// Copyright 2023 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;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.content.Context;
import android.os.PowerManager;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPowerManager;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;

/** Tests for the {@link OmahaServiceStartDelayer}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@Batch(Batch.UNIT_TESTS)
public class OmahaServiceStartDelayerTest {
    @Mock private Activity mActivity;

    @Spy private Runnable mRunnable;
    private OmahaServiceStartDelayer mOmahaServiceStartDelayer;
    private ShadowPowerManager mShadowPowerManager;

    @Before
    public void setUp() throws Exception {
        Context appContext = ApplicationProvider.getApplicationContext();
        MockitoAnnotations.initMocks(this);
        mShadowPowerManager =
                Shadows.shadowOf((PowerManager) appContext.getSystemService(Context.POWER_SERVICE));
        mOmahaServiceStartDelayer = new OmahaServiceStartDelayer();
        mOmahaServiceStartDelayer.setOmahaRunnableForTesting(mRunnable);
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.CREATED);
    }

    @After
    public void tearDown() {
        verifyNoTaskScheduled();

        // In case it has not happened yet forcefully clean up automatically for the next test.
        mOmahaServiceStartDelayer.cancelAndCleanup();
    }

    private void startSession() {
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STARTED);
        mOmahaServiceStartDelayer.onForegroundSessionStart();
    }

    private void stopSession() {
        ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.STOPPED);
        mOmahaServiceStartDelayer.onForegroundSessionEnd();
    }

    private void setInteractive() {
        mShadowPowerManager.setIsInteractive(true);
    }

    private void setNonInteractive() {
        mShadowPowerManager.setIsInteractive(false);
    }

    private void verifyTaskScheduled() {
        Assert.assertTrue(mOmahaServiceStartDelayer.hasRunnableController());
    }

    private void verifyNoTaskScheduled() {
        Assert.assertFalse(mOmahaServiceStartDelayer.hasRunnableController());
    }

    /** Check if the runnable is posted and run while the screen is on. */
    @Test
    @MediumTest
    @Feature({"Omaha"})
    public void testRunnableRunsWithScreenOn() {
        startSession();
        verifyTaskScheduled();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(mRunnable, times(1)).run();
        verifyNoTaskScheduled();
    }

    /** Check that the runnable gets posted and canceled when the app is sent to the background. */
    @Test
    @Feature({"Omaha"})
    public void testRunnableGetsCanceledWhenAppIsBackgrounded() {
        // Starting a session should schedule a delayed task.
        startSession();
        verifyTaskScheduled();

        // Stop happened before the runnable has a chance to run.
        stopSession();
        verifyNoTaskScheduled();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Since the session was stopped before executing the task, the runnable should not have
        // been executed.
        verify(mRunnable, times(0)).run();
    }

    /**
     * Check that the runnable gets posted and canceled when the app is sent to the background, but
     * then restarted when the app goes to the foreground.
     */
    @Test
    @Feature({"Omaha"})
    public void testRunnableExecutesOnlyOnceIfStartedAndStoppedInQuickSuccession() {
        // Starting a session should schedule a delayed task.
        startSession();
        verifyTaskScheduled();
        stopSession();
        verifyNoTaskScheduled();
        startSession();
        verifyTaskScheduled();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // There should in total only be a single execution.
        verify(mRunnable, times(1)).run();
    }

    /** Check that the runnable gets run only while the screen is on. */
    @Test
    @Feature({"Omaha"})
    public void testRunnableGetsRunWhenScreenIsTurnedOn() {
        // Claim the screen is off.
        setNonInteractive();

        // Because the screen is off, nothing should happen.
        startSession();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(mRunnable, times(0)).run();
        verifyNoTaskScheduled();

        // Pretend to turn the screen on and bring the app to the foreground, which should schedule
        // the task.
        setInteractive();
        startSession();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(mRunnable, times(1)).run();
        verifyNoTaskScheduled();
    }

    /** Check that the runnable is not executed while the screen is off, but app is in foreground. */
    @Test
    @Feature({"Omaha"})
    public void testRunnableIsNotRunWhileScreenIsOff() {
        startSession();
        verifyTaskScheduled();

        // Turn screen off without stopping before task is executed.
        setNonInteractive();
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        // Since the screen was off when trying to execute the task, the runnable should not have
        // been executed.
        verify(mRunnable, times(0)).run();
    }

    /** Verify that the runnable is only executed once even if the public API contract is not upheld. */
    @Test
    @MediumTest
    @Feature({"Omaha"})
    public void testRunnableIsOnlyExecutedOnce() {
        startSession();
        verifyTaskScheduled();

        // Start the session again, in case we at some point get two start calls without any
        // stop in between.
        startSession();

        // Now execute the tasks. The runnable should still only be invoked once.
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        verify(mRunnable, times(1)).run();
        verifyNoTaskScheduled();
    }
}