chromium/chrome/android/junit/src/org/chromium/chrome/browser/offlinepages/indicator/OfflineIndicatorMetricsDelegateUnitTest.java

// Copyright 2021 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.offlinepages.indicator;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.UmaRecorderHolder;
import org.chromium.base.test.BaseRobolectricTestRunner;

/** Unit tests for {@link OfflineIndicatorMetricsDelegate}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public final class OfflineIndicatorMetricsDelegateUnitTest {
    /**
     * Fake of OfflineIndicatorMetricsDelegate.Clock used to test metrics that rely on the wall
     * time.
     */
    public static class FakeClock implements OfflineIndicatorMetricsDelegate.Clock {
        private long mCurrentTimeMillis;

        public FakeClock() {
            mCurrentTimeMillis = 0;
        }

        @Override
        public long currentTimeMillis() {
            return mCurrentTimeMillis;
        }

        public void setCurrentTimeMillis(long currentTimeMillis) {
            mCurrentTimeMillis = currentTimeMillis;
        }

        public void advanceCurrentTimeMillis(long millis) {
            mCurrentTimeMillis += millis;
        }
    }

    private FakeClock mFakeClock;

    private OfflineIndicatorMetricsDelegate mMetricsDelegate;

    @Before
    public void setUp() {
        mFakeClock = new FakeClock();
        OfflineIndicatorMetricsDelegate.setClockForTesting(mFakeClock);

        UmaRecorderHolder.resetForTesting();

        resetMetricsDelegate(/* isOffline= */ false, /* isForeground= */ true);
    }

    /**
     * Tests that when the offline indicator is shown and hidden, then we correctly track and record
     * the shown duration.
     */
    @Test
    public void testIndicatorStatusChanged() {
        // Make sure that we aren't tracking anything to start.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the indicator being shown, then after some time it is hidden. In between these
        // two events, we should be tracking a shown duration.
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);
        mMetricsDelegate.onIndicatorHidden();

        // Check that we have stopped tracking a shown duration, and we record the expected values
        // to the histograms.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());
        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 1000);
    }

    /**
     * Tests that when the application state changes while the offline indicator is shown, then we
     * correctly track and record the shown duration and related metrics.
     */
    @Test
    public void testApplicationStateChanged() {
        // Make sure that we aren't tracking anything to start.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the offline indicator being shown, the app being backgrounded, then the app
        // being foregrounded, and finally the offline indicator being hidden.
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);

        mMetricsDelegate.onAppBackgrounded();
        mFakeClock.advanceCurrentTimeMillis(2000);

        mMetricsDelegate.onAppForegrounded();
        mFakeClock.advanceCurrentTimeMillis(4000);

        mMetricsDelegate.onIndicatorHidden();

        // Check that we have stopped tracking a shown duration, and we record the expected values
        // to the histograms.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());
        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 7000);
    }

    /**
     * Tests that when the application state changes multiple times while the offline indicator is
     * shown, then we correctly track and record the shown duration and related metrics.
     */
    @Test
    public void testApplicationStateChanged_RepeatedStateChanges() {
        // Set test constants.
        final int numStateChanges = 10;

        // Make sure that we aren't tracking anything to start.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simluate the offline indicator being shown, and then the app switching between the
        // foreground and background multiple times. Finally simulate the offline indicator being
        // hidden.
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);

        for (int i = 0; i < numStateChanges; i++) {
            mMetricsDelegate.onAppBackgrounded();
            mFakeClock.advanceCurrentTimeMillis(1000);

            mMetricsDelegate.onAppForegrounded();
            mFakeClock.advanceCurrentTimeMillis(1000);
        }

        mMetricsDelegate.onIndicatorHidden();

        // Check that we have stopped tracking a shown duration, and we record the expected values
        // to the histograms.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());
        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2,
                2000 * numStateChanges + 1000);
    }

    /**
     * Tests that we record shown durations correctly even when Chrome is killed in the middle, and
     * we are offline when chrome starts up again. We simulate this by setting |mMetricsDelegate| to
     * null, and then setting it to a new instance of |OfflineIndicatorMetricsDelegate|. The new
     * instance should read the persisted state from prefs, and continue tracking the shown
     * duration.
     */
    @Test
    public void testPersistedMetrics_StartOffline() {
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the indicator being shown, the app being backgrounded, and then app being
        // killed (by setting |mMetricsDelegate| to null).
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);

        mMetricsDelegate.onAppBackgrounded();
        mFakeClock.advanceCurrentTimeMillis(2000);

        mMetricsDelegate = null;
        mFakeClock.advanceCurrentTimeMillis(4000);

        // Simulate Chrome starting up, while still offline. Check that we read values from Prefs
        // and are still tracking a shown duration.
        resetMetricsDelegate(/* isOffline= */ true, /* isForeground= */ true);
        assertTrue(mMetricsDelegate.isTrackingShownDuration());

        // Finally simulate the indicator being hidden.
        mFakeClock.advanceCurrentTimeMillis(8000);
        mMetricsDelegate.onIndicatorHidden();

        // Check that we record the shown duration as the total time between when the indicator was
        // shown to when it was hidden.
        assertFalse(mMetricsDelegate.isTrackingShownDuration());
        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 15000);
    }

    /**
     * Tests that we record shown durations correctly even when Chrome is killed in the middle, and
     * we are online when chrome starts up again. We simulate this by setting |mMetricsDelegate| to
     * * null, and then setting it to a new instance of |OfflineIndicatorMetricsDelegate|. The new
     * instance should read the persisted state from prefs, and continue tracking the shown
     * duration.
     */
    @Test
    public void testPersistedMetrics_StartOnline() {
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the indicator being shown, the app being backgrounded, and then the app being
        // killed (by setting |mMetricsDelegate| to null).
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);

        mMetricsDelegate.onAppBackgrounded();
        mFakeClock.advanceCurrentTimeMillis(2000);

        mMetricsDelegate = null;
        mFakeClock.advanceCurrentTimeMillis(4000);

        // Simulate Chrome starting up, while now online. In this case, we should immediately record
        // the persisted metrics and stop tracking the shown duration..
        resetMetricsDelegate(/* isOffline= */ false, /* isForeground= */ true);
        assertFalse(mMetricsDelegate.isTrackingShownDuration());
        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 7000);
    }

    /** Tests that we clear the persisted state from prefs correctly after tracking a shown duration. */
    @Test
    public void testMetricsCleared() {
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the indicator being shown, then after some being hidden. Check that the expected
        // samples are recorded to the histograms.
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(1000);
        mMetricsDelegate.onAppBackgrounded();
        mFakeClock.advanceCurrentTimeMillis(2000);
        mMetricsDelegate.onAppForegrounded();
        mFakeClock.advanceCurrentTimeMillis(4000);
        mMetricsDelegate.onIndicatorHidden();
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 7000);

        // After checking the histograms, clear them, so our next check only looks at new data.
        UmaRecorderHolder.resetForTesting();

        // Simulate Chrome being killed, then restarted. After restarting, check that we are not be
        // tracking a shown duration.
        mMetricsDelegate = null;
        mFakeClock.advanceCurrentTimeMillis(8000);
        resetMetricsDelegate(/* isOffline= */ false, /* isForeground= */ true);
        mFakeClock.advanceCurrentTimeMillis(16000);
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        // Simulate the indicator being shown, then hidden again. Check that we record the expected
        // value.
        mMetricsDelegate.onIndicatorShown();
        assertTrue(mMetricsDelegate.isTrackingShownDuration());
        mFakeClock.advanceCurrentTimeMillis(32000);
        mMetricsDelegate.onIndicatorHidden();
        assertFalse(mMetricsDelegate.isTrackingShownDuration());

        checkUniqueSample(
                OfflineIndicatorMetricsDelegate.OFFLINE_INDICATOR_SHOWN_DURATION_V2, 32000);
    }

    /**
     * Creates a new instance of |mMetricsDelegate| for testing, and initializes the state based on
     * the input params.
     * @param isOffline Whether |mMetricsDelegate| should start offline (true) or online (false).
     * @param isForeground Whether |mMetricsDelegate| should start foreground (true) or background
     *         (false).
     */
    private void resetMetricsDelegate(boolean isOffline, boolean isForeground) {
        mMetricsDelegate = new OfflineIndicatorMetricsDelegate();
        mMetricsDelegate.onOfflineStateInitialized(isOffline);
        if (isForeground) mMetricsDelegate.onAppForegrounded();
    }

    /**
     * Checks that the given value is the only value recorded to the given histogram.
     * @param histogramName The histogram to check.
     * @param expectedSample The expected value recorded to the histogram.
     */
    private void checkUniqueSample(String histogramName, int expectedSample) {
        assertEquals(1, RecordHistogram.getHistogramTotalCountForTesting(histogramName));
        assertEquals(
                1, RecordHistogram.getHistogramValueCountForTesting(histogramName, expectedSample));
    }
}