chromium/chrome/android/java/src/org/chromium/chrome/browser/metrics/UmaUtils.java

// Copyright 2013 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.metrics;

import android.app.ActivityManager;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.text.format.DateUtils;

import androidx.annotation.IntDef;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;

/** Utilities to support startup metrics - Android version. */
@JNINamespace("chrome::android")
public class UmaUtils {
    /** Observer for this class. */
    public interface Observer {
        /**
         * Called when hasComeToForeground() changes from false to true for the first time after
         * post-native initialization has started.
         */
        void onHasComeToForegroundWithNative();
    }

    private static Observer sObserver;

    /** Sets the observer. */
    public static void setObserver(Observer observer) {
        ThreadUtils.assertOnUiThread();
        assert sObserver == null;
        sObserver = observer;
    }

    /** Removes the observer. */
    public static void removeObserver() {
        ThreadUtils.assertOnUiThread();
        sObserver = null;
    }

    // All these values originate from SystemClock.uptimeMillis().
    private static long sApplicationStartTimeMs;
    private static long sForegroundStartWithNativeTimeMs;
    private static long sBackgroundWithNativeTimeMs;

    private static boolean sSkipRecordingNextForegroundStartTimeForTesting;

    // Will short-circuit out of the next recordForegroundStartTimeWithNative() call.
    public static void skipRecordingNextForegroundStartTimeForTesting() {
        sSkipRecordingNextForegroundStartTimeForTesting = true;
    }

    /**
     * App standby bucket status, used for UMA reporting. Enum values correspond to the return
     * values of {@link UsageStatsManager#getAppStandbyBucket}.
     * These values are persisted to logs. Entries should not be renumbered and
     * numeric values should never be reused.
     */
    @IntDef({
        StandbyBucketStatus.ACTIVE,
        StandbyBucketStatus.WORKING_SET,
        StandbyBucketStatus.FREQUENT,
        StandbyBucketStatus.RARE,
        StandbyBucketStatus.RESTRICTED,
        StandbyBucketStatus.UNSUPPORTED,
        StandbyBucketStatus.EXEMPTED,
        StandbyBucketStatus.NEVER,
        StandbyBucketStatus.OTHER,
        StandbyBucketStatus.COUNT
    })
    private @interface StandbyBucketStatus {
        int ACTIVE = 0;
        int WORKING_SET = 1;
        int FREQUENT = 2;
        int RARE = 3;
        int RESTRICTED = 4;
        int UNSUPPORTED = 5;
        int EXEMPTED = 6;
        int NEVER = 7;
        int OTHER = 8;
        int COUNT = 9;
    }

    /**
     * Record the time in the application lifecycle at which Chrome code first runs
     * (Application.attachBaseContext()).
     */
    public static void recordMainEntryPointTime() {
        // We can't simply pass this down through a JNI call, since the JNI for chrome
        // isn't initialized until we start the native content browser component, and we
        // then need the start time in the C++ side before we return to Java. As such we
        // save it in a static that the C++ can fetch once it has initialized the JNI.
        sApplicationStartTimeMs = SystemClock.uptimeMillis();
    }

    /**
     * Record the time at which Chrome was brought to foreground. Should be recorded only after
     * post-native initialization has started.
     *
     * A notable exception is FRE. It records foreground time in OnResume(), which can happen before
     * native. It was made in 2016 to allow native initialization in FRE without errors. See
     * http://crrev.com/436530.
     */
    public static void recordForegroundStartTimeWithNative() {
        if (sSkipRecordingNextForegroundStartTimeForTesting) {
            sSkipRecordingNextForegroundStartTimeForTesting = false;
            return;
        }

        // Since this can be called from multiple places (e.g. ChromeActivitySessionTracker
        // and FirstRunActivity), only set the time if it hasn't been set previously or if
        // Chrome has been sent to background since the last foreground time.
        if (sForegroundStartWithNativeTimeMs == 0
                || sForegroundStartWithNativeTimeMs < sBackgroundWithNativeTimeMs) {
            if (sObserver != null && sForegroundStartWithNativeTimeMs == 0) {
                sObserver.onHasComeToForegroundWithNative();
            }
            sForegroundStartWithNativeTimeMs = SystemClock.uptimeMillis();
        }
    }

    /**
     * Record the time at which Chrome was sent to background.
     *
     * Should not be called before post-native initialization.
     */
    public static void recordBackgroundTimeWithNative() {
        sBackgroundWithNativeTimeMs = SystemClock.uptimeMillis();
    }

    /**
     * Determines whether Chrome was brought to foreground after post-native initialization started.
     */
    public static boolean hasComeToForegroundWithNative() {
        return sForegroundStartWithNativeTimeMs != 0;
    }

    /** Determines if Chrome was brought to background. */
    public static boolean hasComeToBackgroundWithNative() {
        return sBackgroundWithNativeTimeMs != 0;
    }

    /**
     * Determines if this client is eligible to send metrics based on sampling. If it is, and there
     * was user consent, then metrics should be reported.
     */
    public static boolean isClientInSampleForMetrics() {
        return UmaUtilsJni.get().isClientInSampleForMetrics();
    }

    /**
     * Determines if this client is eligible to send crashes based on sampling. If it is, and there
     * was user consent, then crashes should be reported.
     */
    public static boolean isClientInSampleForCrashes() {
        return UmaUtilsJni.get().isClientInSampleForCrashes();
    }

    /** Records various levels of background restrictions imposed by android on chrome. */
    public static void recordBackgroundRestrictions() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return;
        Context context = ContextUtils.getApplicationContext();
        ActivityManager activityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        boolean isBackgroundRestricted = activityManager.isBackgroundRestricted();
        RecordHistogram.recordBooleanHistogram(
                "Android.BackgroundRestrictions.IsBackgroundRestricted", isBackgroundRestricted);

        int standbyBucketUma = getStandbyBucket(context);
        RecordHistogram.recordEnumeratedHistogram(
                "Android.BackgroundRestrictions.StandbyBucket",
                standbyBucketUma,
                StandbyBucketStatus.COUNT);

        if (isBackgroundRestricted) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Android.BackgroundRestrictions.StandbyBucket.WithUserRestriction",
                    standbyBucketUma,
                    StandbyBucketStatus.COUNT);
        }
    }

    /** Record minidump uploading time split by background restriction status. */
    public static void recordMinidumpUploadingTime(long taskDurationMs) {
        RecordHistogram.recordCustomTimesHistogram(
                "Stability.Android.MinidumpUploadingTime",
                taskDurationMs,
                1,
                DateUtils.DAY_IN_MILLIS,
                50);
    }

    private static @StandbyBucketStatus int getStandbyBucket(Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return StandbyBucketStatus.UNSUPPORTED;

        UsageStatsManager usageStatsManager =
                (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
        int standbyBucket = usageStatsManager.getAppStandbyBucket();
        int standbyBucketUma;
        switch (standbyBucket) {
            case UsageStatsManager.STANDBY_BUCKET_ACTIVE:
                standbyBucketUma = StandbyBucketStatus.ACTIVE;
                break;
            case UsageStatsManager.STANDBY_BUCKET_WORKING_SET:
                standbyBucketUma = StandbyBucketStatus.WORKING_SET;
                break;
            case UsageStatsManager.STANDBY_BUCKET_FREQUENT:
                standbyBucketUma = StandbyBucketStatus.FREQUENT;
                break;
            case UsageStatsManager.STANDBY_BUCKET_RARE:
                standbyBucketUma = StandbyBucketStatus.RARE;
                break;
            case UsageStatsManager.STANDBY_BUCKET_RESTRICTED:
                standbyBucketUma = StandbyBucketStatus.RESTRICTED;
                break;
            case 5: // STANDBY_BUCKET_EXEMPTED
                standbyBucketUma = StandbyBucketStatus.EXEMPTED;
                break;
            case 50: // STANDBY_BUCKET_NEVER
                standbyBucketUma = StandbyBucketStatus.NEVER;
                break;
            default:
                standbyBucketUma = StandbyBucketStatus.OTHER;
                break;
        }
        return standbyBucketUma;
    }

    /**
     * Sets whether metrics reporting was opt-in or not. If it was opt-in, then the enable checkbox
     * on first-run was default unchecked. If it was opt-out, then the checkbox was default checked.
     * This should only be set once, and only during first-run.
     */
    public static void recordMetricsReportingDefaultOptIn(boolean optIn) {
        UmaUtilsJni.get().recordMetricsReportingDefaultOptIn(optIn);
    }

    @CalledByNative
    public static long getApplicationStartTime() {
        return sApplicationStartTimeMs;
    }

    @CalledByNative
    public static long getProcessStartTime() {
        return Process.getStartUptimeMillis();
    }

    @NativeMethods
    interface Natives {
        boolean isClientInSampleForMetrics();

        boolean isClientInSampleForCrashes();

        void recordMetricsReportingDefaultOptIn(boolean optIn);
    }
}