chromium/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/indicator/OfflineIndicatorMetricsDelegate.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 org.chromium.base.ResettersForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;

/**
 * Tracks metrics about how long the Offline Indicator is shown. All state related to the metrics
 * are persisted to prefs, so if Chrome is ever killed while the Offline Indicator is shown, we can
 * continue tracking accurate metrics the next time Chrome is started up.
 */
public class OfflineIndicatorMetricsDelegate {
    /** Clock to use so we can mock the time in tests. */
    public interface Clock {
        long currentTimeMillis();
    }

    private static Clock sClock = System::currentTimeMillis;

    // UMA Histograms.
    public static final String OFFLINE_INDICATOR_SHOWN_DURATION_V2 =
            "OfflineIndicator.ShownDurationV2";

    /** Whether or not we are tracking a shown duration of the offline indicator. */
    private boolean mIsTrackingShownDuration;

    /** Whether or not the application is in the foreground (true) or background (false) */
    private boolean mIsApplicationForeground;

    /**
     * The wall time in milliseconds of the most recent time the offline indicator began being
     * shown. This value is persisted in prefs if |mIsTrackingShownDuration| is true.
     */
    private long mIndicatorShownWallTimeMs;

    /**
     * While tracking a shown duration, the wall time in miiliseconds of the most recent state
     * change (offline, online, foreground, or background). This value is persisted in prefs when
     * updated.
     */
    private long mLastUpdateWallTimeMs;

    /**
     * While tracking a shown duration, the total time, in milliseconds, that the application was
     * in foreground. This value is persisted in prefs when updated.
     */
    private long mTimeInForegroundMs;

    /**
     * While tracking a shown duration, the total time, in milliseconds, that the application was
     * in background. This value is persisted in prefs when updated.
     */
    private long mTimeInBackgroundMs;

    /**
     * While tracking a shown duration, the time, in milliseconds, that the application was
     * in foreground until the first time it was backgrounded. This value is persisted in prefs when
     * updated.
     */
    private long mFirstTimeInForegroundMs;

    /**
     * While tracking a shown duration, the number of times the application changes from foreground
     * to background. This value is persisted in prefs when updated.
     */
    private int mNumTimesBackgrounded;

    public OfflineIndicatorMetricsDelegate() {
        SharedPreferencesManager sharedPreferencesManager = ChromeSharedPreferences.getInstance();

        // Read stored state from Prefs
        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_WALL_TIME_SHOWN_MS)) {
            mIndicatorShownWallTimeMs =
                    sharedPreferencesManager.readLong(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_WALL_TIME_SHOWN_MS);
            mIsTrackingShownDuration = true;
        }

        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS)) {
            mLastUpdateWallTimeMs =
                    sharedPreferencesManager.readLong(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS);
        }

        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_FOREGROUND_MS)) {
            mTimeInForegroundMs =
                    sharedPreferencesManager.readLong(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_FOREGROUND_MS);
        }

        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_BACKGROUND_MS)) {
            mTimeInBackgroundMs =
                    sharedPreferencesManager.readLong(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_BACKGROUND_MS);
        }

        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_FIRST_TIME_IN_FOREGROUND_MS)) {
            mFirstTimeInForegroundMs =
                    sharedPreferencesManager.readLong(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_FIRST_TIME_IN_FOREGROUND_MS);
        }

        if (sharedPreferencesManager.contains(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_NUM_TIMES_BACKGROUNDED)) {
            mNumTimesBackgrounded =
                    sharedPreferencesManager.readInt(
                            ChromePreferenceKeys.OFFLINE_INDICATOR_V2_NUM_TIMES_BACKGROUNDED);
        }
    }

    /**
     * Returns whether we are currently tracking a shown duration of the offline indicator or not.
     */
    public boolean isTrackingShownDuration() {
        return mIsTrackingShownDuration;
    }

    /**
     * When we are tracking a persisted shown duration and the offline state is initialized to
     * online, then we treat this as if the offline indicator was just hidden.
     * @param isOffline The offline state at initialization.
     */
    public void onOfflineStateInitialized(boolean isOffline) {
        if (mIsTrackingShownDuration && !isOffline) {
            onIndicatorHidden();
        }
    }

    /**
     * When the Offline Indicator is shown, then we begin tracking a shown duration if not already
     * tracking one. This function can be called when already tracking a shown duration, if there
     * was state stored in Prefs when Chrome started up.
     */
    public void onIndicatorShown() {
        if (mIsTrackingShownDuration) return;

        SharedPreferencesManager sharedPreferencesManager = ChromeSharedPreferences.getInstance();

        long currentTimeMs = sClock.currentTimeMillis();

        mIsTrackingShownDuration = true;
        mIndicatorShownWallTimeMs = currentTimeMs;
        sharedPreferencesManager.writeLong(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_WALL_TIME_SHOWN_MS,
                mIndicatorShownWallTimeMs);

        mLastUpdateWallTimeMs = currentTimeMs;
        sharedPreferencesManager.writeLong(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS,
                mLastUpdateWallTimeMs);
    }

    /**
     * When the Offline Indicator is hidden, then we record the shown duration. If the indicator is
     * not shown when Chrome starts up and it was shown the last time Chrome was running in the
     * foreground, then we count the indicator as being hidden.
     */
    public void onIndicatorHidden() {
        if (!mIsTrackingShownDuration) return;

        if (mIsApplicationForeground) {
            updateForegroundPeriod();
        } else {
            updateBackgroundPeriod();
        }

        final long shownDurationWallTimeMs = sClock.currentTimeMillis() - mIndicatorShownWallTimeMs;

        // shownDurationWallTimeMs can be negative in cases where the system time is changed, so
        // we want to avoid recording metrics in cases where we know this happened.
        if (shownDurationWallTimeMs >= 0) {
            recordShownDurationHistograms();
        }

        reset();
    }

    /**
     * When the application is foregrounded, we update the state used to track how long the
     * indicator is shown while in the foreground or in the background.
     */
    public void onAppForegrounded() {
        if (mIsTrackingShownDuration) {
            updateBackgroundPeriod();
        }
        mIsApplicationForeground = true;
    }

    /**
     * When the application is backgrounded, we update the state used to track how long the
     * indicator is shown while in the foreground or in the background.
     */
    public void onAppBackgrounded() {
        if (mIsTrackingShownDuration) {
            updateForegroundPeriod();

            // Updates state based on the foreground to background transition.
            SharedPreferencesManager sharedPreferencesManager =
                    ChromeSharedPreferences.getInstance();
            if (!sharedPreferencesManager.contains(
                    ChromePreferenceKeys.OFFLINE_INDICATOR_V2_FIRST_TIME_IN_FOREGROUND_MS)) {
                mFirstTimeInForegroundMs = mTimeInForegroundMs;
                sharedPreferencesManager.writeLong(
                        ChromePreferenceKeys.OFFLINE_INDICATOR_V2_FIRST_TIME_IN_FOREGROUND_MS,
                        mFirstTimeInForegroundMs);
            }

            mNumTimesBackgrounded++;
            sharedPreferencesManager.writeInt(
                    ChromePreferenceKeys.OFFLINE_INDICATOR_V2_NUM_TIMES_BACKGROUNDED,
                    mNumTimesBackgrounded);
        }
        mIsApplicationForeground = false;
    }

    /**
     * Updates the tracked metrics given that the app has been in the foreground from
     * |mLastUpdateWallTimeMs| until now. Metrics are persisted to prefs when updated.
     */
    private void updateForegroundPeriod() {
        SharedPreferencesManager sharedPreferencesManager = ChromeSharedPreferences.getInstance();

        long currentTimeMs = sClock.currentTimeMillis();
        long timeSinceLastUpdateMs = currentTimeMs - mLastUpdateWallTimeMs;

        mTimeInForegroundMs += timeSinceLastUpdateMs;
        sharedPreferencesManager.writeLong(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_FOREGROUND_MS,
                mTimeInForegroundMs);

        mLastUpdateWallTimeMs = currentTimeMs;
        sharedPreferencesManager.writeLong(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS,
                mLastUpdateWallTimeMs);
    }

    /**
     * Updates the tracked metrics given that the app has been in the background from
     * |mLastUpdateWallTimeMs| until now. Metrics are persisted to prefs when updated.
     */
    private void updateBackgroundPeriod() {
        SharedPreferencesManager sharedPreferencesManager = ChromeSharedPreferences.getInstance();

        long currentTimeMs = sClock.currentTimeMillis();
        long timeSinceLastUpdateMs = currentTimeMs - mLastUpdateWallTimeMs;

        mTimeInBackgroundMs += timeSinceLastUpdateMs;
        sharedPreferencesManager.writeLong(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_BACKGROUND_MS,
                mTimeInBackgroundMs);

        mLastUpdateWallTimeMs = currentTimeMs;
        ChromeSharedPreferences.getInstance()
                .writeLong(
                        ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS,
                        mLastUpdateWallTimeMs);
    }

    /**
     * Records the set of UMA histograms related to the shown duration of the Offline indicator.
     * Some of the histograms are only recorded in certain cases.
     */
    private void recordShownDurationHistograms() {
        RecordHistogram.recordLongTimesHistogram100(
                OFFLINE_INDICATOR_SHOWN_DURATION_V2, mTimeInForegroundMs + mTimeInBackgroundMs);

        if (!ChromeSharedPreferences.getInstance()
                .contains(ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_BACKGROUND_MS)) {
            assert mNumTimesBackgrounded == 0;
        }
    }

    /**
     * Resets the state of this instance. Sets the member variables to zero or false, and clears the
     * persisted values from prefs.
     */
    private void reset() {
        mIndicatorShownWallTimeMs = 0;
        mLastUpdateWallTimeMs = 0;
        mTimeInForegroundMs = 0;
        mTimeInBackgroundMs = 0;
        mFirstTimeInForegroundMs = 0;
        mNumTimesBackgrounded = 0;
        mIsTrackingShownDuration = false;

        SharedPreferencesManager sharedPreferencesManager = ChromeSharedPreferences.getInstance();
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_WALL_TIME_SHOWN_MS);
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_LAST_UPDATE_WALL_TIME_MS);
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_FOREGROUND_MS);
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_TIME_IN_BACKGROUND_MS);
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_FIRST_TIME_IN_FOREGROUND_MS);
        sharedPreferencesManager.removeKey(
                ChromePreferenceKeys.OFFLINE_INDICATOR_V2_NUM_TIMES_BACKGROUNDED);
    }

    static void setClockForTesting(Clock clock) {
        var oldValue = sClock;
        sClock = clock;
        ResettersForTesting.register(() -> sClock = oldValue);
    }
}