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

// Copyright 2020 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 static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.offlinepages.indicator.OfflineDetector.STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS;
import static org.chromium.chrome.browser.offlinepages.indicator.OfflineDetector.STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS;
import static org.chromium.chrome.browser.offlinepages.indicator.OfflineDetector.setMockElapsedTimeSupplier;

import android.app.Application;
import android.content.ContentResolver;
import android.os.Handler;
import android.provider.Settings;

import androidx.test.core.app.ApplicationProvider;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ApplicationState;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.net.connectivitydetector.ConnectivityDetector;
import org.chromium.chrome.browser.net.connectivitydetector.ConnectivityDetector.ConnectionState;

/** Unit tests for {@link OfflineDetector}. */
@RunWith(BaseRobolectricTestRunner.class)
public class OfflineDetectorUnitTest {
    @Mock private ConnectivityDetector mConnectivityDetector;
    @Mock private Handler mHandler;

    private long mElapsedTimeMs;
    private OfflineDetector mOfflineDetector;

    private int mIsOfflineNotificationsReceivedByObserver;
    private boolean mLastNotificationReceivedIsOffline;

    private int mIsForegroundNotificationsReceivedByObserver;
    private boolean mLastNotificationReceivedIsForeground;

    private ContentResolver mContentResolver;
    private Application mContext;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mContext = ApplicationProvider.getApplicationContext();
        mContentResolver = mContext.getContentResolver();

        mElapsedTimeMs = 0;
        OfflineDetector.setMockElapsedTimeSupplier(() -> mElapsedTimeMs);

        OfflineDetector.setMockConnectivityDetector(mConnectivityDetector);

        mOfflineDetector =
                new OfflineDetector(
                        (Boolean offline) -> onConnectionStateChanged(offline),
                        (Boolean isForeground) -> onApplicationStateChanged(isForeground),
                        mContext);
        mOfflineDetector.setHandlerForTesting(mHandler);
    }

    @After
    public void tearDown() {
        OfflineDetector.setMockElapsedTimeSupplier(null);
        Settings.System.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON, 0);
    }

    /**
     * Tests that the online notification is sent immediately when the device goes online.
     * Also, verifies that the offline notification is sent after
     * |STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS| time has elapsed.
     */
    @Test
    public void testCallbackInvokedOnConnectionChange() {
        // Set the app state to foreground before running the tests.
        changeApplicationStateToBackground(false);
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);

        // Change to online.
        changeConnectionState(false);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        // Change to offline.
        changeConnectionState(true);
        assertEquals(
                "Notification received immediately after connection changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to offline",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS);
        captor.getValue().run();

        assertEquals(
                "Notification count not updated after connection changed to offline",
                2,
                mIsOfflineNotificationsReceivedByObserver);
        assertTrue(
                "Notification not received after connection changed to offline",
                mLastNotificationReceivedIsOffline);

        // Change to online.
        changeConnectionState(false);
        assertEquals(
                "Notification count not updated after connection changed to online",
                3,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification not received after connection changed to online",
                mLastNotificationReceivedIsOffline);

        // Change to online again. It should not trigger a notification.
        changeConnectionState(false);
        assertEquals(
                "Extra notification received even though there is no change in connection state",
                3,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though there is no change in connection state",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Reports the device as offline while the app is in background. Then, after a long time, the
     * app is brought to foreground. The test verifies that the offline callback is invoked after
     * |STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS|.
     */
    @Test
    public void testCallbackNotInvokedOfflineBackgroundToForeground() {
        // Change to online.
        changeConnectionState(false);
        assertEquals(0, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        assertEquals(
                "Notification received immediately after connection changed to offline",
                0,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to offline.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Report the app as backgrounded and then report the device as offline. This is the
        // behavior experienced by apps that are prohibited from using data while in background.
        changeApplicationStateToBackground(true);
        changeConnectionState(true);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // Report the app state as foregrounded. The offline state should not be notified
        // immediately.
        changeApplicationStateToBackground(false);

        verify(mHandler)
                .postDelayed(captor.capture(), eq(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS));

        assertEquals(
                "Extra notification received even though app just returned to foreground",
                0,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though app just returned to foreground",
                mLastNotificationReceivedIsOffline);

        // Advance time after which the offline state should be notified.
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Expected notification when app has been in foreground for long",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertTrue(
                "Expected notification when app has been in foreground for long",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Reports the device as offline while the app is in background. Then, after a long time, the
     * app is brought to foreground. The test verifies that the offline callback is not invoked
     * immediately and that the callback is cancelled if the device state is reported as online in
     * the meantime.
     */
    @Test
    public void testCallbackNotInvokedOfflineBackgroundToForegroundBatterySaver() {
        // Change to online.
        changeConnectionState(false);
        assertEquals(0, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        assertEquals(
                "Notification received immediately after connection changed to offline",
                0,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to offline.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Report the app as backgrounded and then report the device as offline. This is the
        // behavior experienced by apps that are prohibited from using data while in background.
        changeApplicationStateToBackground(true);
        changeConnectionState(true);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // Bring the app to foreground. The offline status should not be notified immediately.
        changeApplicationStateToBackground(false);

        verify(mHandler)
                .postDelayed(captor.capture(), eq(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS));

        assertEquals(
                "Extra notification received even though connection is still effectively online",
                0,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Connection is reported as offline when it's online",
                mLastNotificationReceivedIsOffline);

        // Received online notification. This should cancel the pending offline callback.
        changeConnectionState(false);

        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Extra notification received even though connection is still online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Connection is reported as online when it's offline",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Reports the device as offline followed by online while the app is in background. Then, after
     * a long time, the app is brought to foreground. The test verifies that the online callback is
     * invoked immediately.
     */
    @Test
    public void testCallbackInvokedOnlineBackgroundToForeground() {
        // Set the app state to foreground before running the tests.
        changeApplicationStateToBackground(false);
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);

        // Change to online.
        changeConnectionState(false);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        assertEquals(
                "Duplicate notification received after connection changed to online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Duplicate notification received after connection changed to online.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Report the connection as offline and then app as backgrounded.
        changeConnectionState(true);
        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Notification not received even though connection is now offline",
                2,
                mIsOfflineNotificationsReceivedByObserver);
        assertTrue(
                "Notification not received even though connection is now offline",
                mLastNotificationReceivedIsOffline);

        // Advance time by a long duration (10 minutes) and report app as backgrounded.
        advanceTimeByMs(10 * 60 * 1000);
        changeApplicationStateToBackground(true);

        // Report the connection as online and move clock.
        changeConnectionState(false);
        advanceTimeByMs(10 * 60 * 1000);

        // Report the app state as foregrounded. The online state should be notified
        // immediately.
        changeApplicationStateToBackground(false);
        captor.getValue().run();
        assertEquals(
                "Notification not received even though connection is now online",
                3,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification not received even though connection is now online",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Tests that when the device switches immediately from offline to online, then the callback is
     * not executed.
     */
    @Test
    public void testCallbackNotInvokedOfflineToFastOnline() {
        // Set the app state to foreground before running the tests.
        changeApplicationStateToBackground(false);
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);

        // Change to online.
        changeConnectionState(false);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        // Change to offline.
        changeConnectionState(true);
        assertEquals(
                "Notification received immediately after connection changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to offline.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));
        advanceTimeByMs(
                STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS - 1000L);

        assertEquals(
                "Notification received soon after connection changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received soon after connection changed to offline",
                mLastNotificationReceivedIsOffline);

        // Change to online.
        changeConnectionState(false);
        assertEquals(
                "Extra notification received after connection changed to online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Connection is reported as offline when it's online",
                mLastNotificationReceivedIsOffline);

        // Move clock forward by 1000ms so that the offline callback posts after
        // |STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS|. This should not trigger a notification
        // since the connection is now online.
        advanceTimeByMs(1000L);
        captor.getValue().run();
        assertEquals(
                "Extra notification received even though connection is still online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Connection is reported as offline when it's online",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Waits |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| before
     * reporting the device as offline if the app has been on an online connection before.
     */
    @Test
    public void testCallbackNotInvokedOfflineInForeground() {
        changeApplicationStateToBackground(false);
        // Change to online.
        changeConnectionState(false);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        assertEquals(
                "Notification received immediately after connection changed to online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to online.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // The offline state should not be notified after
        // |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| since the device
        // has been in the foreground for a long time.
        changeConnectionState(true);

        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));

        assertEquals(
                "Extra notification received even though device just changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device just changed to offline",
                mLastNotificationReceivedIsOffline);

        // Advance time after which the offline state should be notified.
        advanceTimeByMs(
                STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS
                        - STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Expected notification when app has been offline for long",
                2,
                mIsOfflineNotificationsReceivedByObserver);
        assertTrue(
                "Expected notification when app has been offline for long",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Waits |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| before
     * reporting the device as offline if the app has been on an online connection before. If the
     * device is reported as online in the meantime, then the posted task is cancelled and the
     * callback is not invoked.
     */
    @Test
    public void testCallbackNotInvokedOfflineInForegroundCancelled() {
        changeApplicationStateToBackground(false);
        // Change to online.
        changeConnectionState(false);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        assertEquals(
                "Notification received immediately after connection changed to online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Notification received immediately after connection changed to online.",
                mLastNotificationReceivedIsOffline);
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // The offline state should not be notified after
        // |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| since the device
        // has been in the foreground for a long time.
        changeConnectionState(true);

        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));

        assertEquals(
                "Extra notification received even though device just changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device just changed to offline",
                mLastNotificationReceivedIsOffline);

        // Change to online before
        // |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| duration is over.
        // This should cancel the callbacks.
        changeConnectionState(false);

        // Advance time after which the offline state should be notified.
        advanceTimeByMs(
                STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS
                        - STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Extra notification received even though device is now online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device is now online",
                mLastNotificationReceivedIsOffline);
    }

    /**
     * Tests that the application state callback is called when the application switches between
     * background and foreground.
     */
    @Test
    public void testApplicationStateCallback() {
        changeApplicationStateToBackground(false);

        assertEquals(
                "Notification received when application state changes",
                1,
                mIsForegroundNotificationsReceivedByObserver);
        assertTrue(
                "Last notification received from application going to foreground",
                mLastNotificationReceivedIsForeground);
        assertTrue(
                "Stored state matches last notification",
                mOfflineDetector.isApplicationForeground());

        changeApplicationStateToBackground(false);

        assertEquals(
                "No notification received if application state doesn't change",
                1,
                mIsForegroundNotificationsReceivedByObserver);
        assertTrue(
                "Last notification received from application going to foreground",
                mLastNotificationReceivedIsForeground);
        assertTrue(
                "Stored state matches last notification",
                mOfflineDetector.isApplicationForeground());

        changeApplicationStateToBackground(true);

        assertEquals(
                "Notification received when application state changes",
                2,
                mIsForegroundNotificationsReceivedByObserver);
        assertFalse(
                "Last notification received from application going to background",
                mLastNotificationReceivedIsForeground);
        assertFalse(
                "Stored state matches last notification",
                mOfflineDetector.isApplicationForeground());

        changeApplicationStateToBackground(true);

        assertEquals(
                "No notification received if application state doesn't change",
                2,
                mIsForegroundNotificationsReceivedByObserver);
        assertFalse(
                "Last notification received from application going to background",
                mLastNotificationReceivedIsForeground);
        assertFalse(
                "Stored state matches last notification",
                mOfflineDetector.isApplicationForeground());

        changeApplicationStateToBackground(false);

        assertEquals(
                "Notification received when application state changes",
                3,
                mIsForegroundNotificationsReceivedByObserver);
        assertTrue(
                "Last notification received from application going to foreground",
                mLastNotificationReceivedIsForeground);
        assertTrue(
                "Stored state matches last notification",
                mOfflineDetector.isApplicationForeground());
    }

    @Test
    public void testAirplaneModeToOffline() {
        changeApplicationStateToBackground(false);

        // Simulate offline + airplane mode.
        Settings.System.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON, 1);
        changeConnectionState(true);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // Simulate airplane mode change to false, while still offline.
        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
        Settings.System.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON, 0);
        changeConnectionState(true);

        // Offline status shouldn't be communicated until
        // SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS elapses.
        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));

        assertEquals(
                "Extra notification received even though device just changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device just changed to offline",
                mLastNotificationReceivedIsOffline);

        // Verify offline status is communicated if time elaspses.
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS);
        captor.getValue().run();

        assertEquals(
                "Notification count not updated after connection changed to offline",
                2,
                mIsOfflineNotificationsReceivedByObserver);
        assertTrue(
                "Notification not received after connection changed to offline",
                mLastNotificationReceivedIsOffline);
    }

    @Test
    public void testAirplaneModeToOnline() {
        changeApplicationStateToBackground(false);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);

        // Start online.
        changeConnectionState(true);
        assertEquals(1, mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        verify(mHandler)
                .postDelayed(captor.capture(), eq(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS));

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // Simulate offline + airplane mode.
        Settings.System.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON, 1);
        changeConnectionState(true);

        // #updateState will still run again since connection state has changed.
        verify(mHandler, times(2))
                .postDelayed(captor.capture(), eq(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS));

        // Advance time after which runnable will execute
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_OFFLINE_DURATION_MS);
        captor.getValue().run();

        // Effective offline status hasn't changed.
        assertEquals(
                "Effective offline status didn't change",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(mLastNotificationReceivedIsOffline);

        // Advance time by a long duration (10 minutes).
        advanceTimeByMs(10 * 60 * 1000);

        // Simulate airplane mode change to false, while still offline.
        Settings.System.putInt(mContentResolver, Settings.Global.AIRPLANE_MODE_ON, 0);
        changeConnectionState(true);

        // Offline status shouldn't be communicated until
        // SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS elapses.
        verify(mHandler)
                .postDelayed(
                        captor.capture(),
                        eq(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS));

        assertEquals(
                "Extra notification received even though device just changed to offline",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device just changed to offline",
                mLastNotificationReceivedIsOffline);

        // Change to online before
        // |STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS| duration is over.
        // This should cancel the callbacks.
        changeConnectionState(false);

        // Advance time after which the offline state should be notified.
        advanceTimeByMs(STATUS_INDICATOR_WAIT_ON_SWITCH_ONLINE_TO_OFFLINE_DEFAULT_DURATION_MS);
        captor.getValue().run();
        assertEquals(
                "Extra notification received even though device is now online",
                1,
                mIsOfflineNotificationsReceivedByObserver);
        assertFalse(
                "Extra notification received even though device is now online",
                mLastNotificationReceivedIsOffline);
    }

    private void changeConnectionState(boolean offline) {
        final int state = offline ? ConnectionState.NO_INTERNET : ConnectionState.VALIDATED;
        mOfflineDetector.onConnectionStateChanged(state);
    }

    private void changeApplicationStateToBackground(boolean changeToBackground) {
        mOfflineDetector.onApplicationStateChange(
                changeToBackground
                        ? ApplicationState.HAS_STOPPED_ACTIVITIES
                        : ApplicationState.HAS_RUNNING_ACTIVITIES);
    }

    private void advanceTimeByMs(long delta) {
        mElapsedTimeMs += delta;
        setMockElapsedTimeSupplier(() -> mElapsedTimeMs);
    }

    private void onConnectionStateChanged(boolean offline) {
        mIsOfflineNotificationsReceivedByObserver++;
        mLastNotificationReceivedIsOffline = offline;
    }

    private void onApplicationStateChanged(boolean isForeground) {
        mIsForegroundNotificationsReceivedByObserver++;
        mLastNotificationReceivedIsForeground = isForeground;
    }
}