chromium/chrome/browser/android/metrics/uma_session_stats.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/android/metrics/uma_session_stats.h"

#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/no_destructor.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/android/metrics/android_session_durations_service.h"
#include "chrome/browser/android/metrics/android_session_durations_service_factory.h"
#include "chrome/browser/android/preferences/shared_preferences_migrator_android.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/installer/util/google_update_settings.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/metrics/metrics_service.h"
#include "components/metrics_services_manager/metrics_services_manager.h"
#include "components/prefs/pref_service.h"
#include "components/ukm/ukm_service.h"
#include "components/variations/synthetic_trial_registry.h"
#include "content/public/browser/browser_thread.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/UmaSessionStats_jni.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::JavaParamRef;
using base::UserMetricsAction;

namespace {
// Used to keep the state of whether we should consider metric consent enabled.
// This is used/read only within the ChromeMetricsServiceAccessor methods.
bool g_metrics_consent_for_testing = false;
}  // namespace

namespace {
// Counter for the number of times onPreCreate and onResume were called between
// foreground sessions that reach native code. The code PXRY means:
// * onPreCreate was called X times
// * onResume was called Y times
// * the counters are capped at 3, so that value means "3 or more".
enum class ChromeActivityCounter : int32_t {
  P0R0 = 0,
  P0R1 = 1,
  P0R2 = 2,
  P0R3 = 3,
  P1R0 = 4,
  P1R1 = 5,
  P1R2 = 6,
  P1R3 = 7,
  P2R0 = 8,
  P2R1 = 9,
  P2R2 = 10,
  P2R3 = 11,
  P3R0 = 12,
  P3R1 = 13,
  P3R2 = 14,
  P3R3 = 15,
  kMaxValue = 15,
};
}  // namespace

void UmaSessionStats::UmaResumeSession(JNIEnv* env,
                                       const JavaParamRef<jobject>& obj) {
  DCHECK(g_browser_process);
  if (++active_session_count_ == 1) {
    const bool had_background_session =
        session_time_tracker_.BeginForegroundSession();

    // Tell the metrics services that the application resumes.
    metrics::MetricsService* metrics = g_browser_process->metrics_service();
    if (metrics) {
      // Forcing a new log allows foreground and background metrics can be
      // separated in analysis.
      const bool force_new_log = base::FeatureList::IsEnabled(
                                     chrome::android::kUmaBackgroundSessions) &&
                                 had_background_session;

      metrics->OnAppEnterForeground(force_new_log);
    }
    // Report background session time if it wasn't already reported by
    // OnAppEnterForeground() -> ProvideCurrentSessionData().
    session_time_tracker_.ReportBackgroundSessionTime();

    ukm::UkmService* ukm_service =
        g_browser_process->GetMetricsServicesManager()->GetUkmService();
    if (ukm_service)
      ukm_service->OnAppEnterForeground();

    AndroidSessionDurationsServiceFactory::OnAppEnterForeground(
        session_time_tracker_.session_start_time());
  }
}

void UmaSessionStats::UmaEndSession(JNIEnv* env,
                                    const JavaParamRef<jobject>& obj) {
  --active_session_count_;
  DCHECK_GE(active_session_count_, 0);

  if (active_session_count_ == 0) {
    const base::TimeDelta duration =
        session_time_tracker_.EndForegroundSession();

    DCHECK(g_browser_process);
    // Tell the metrics services they were cleanly shutdown.
    metrics::MetricsService* metrics = g_browser_process->metrics_service();
    if (metrics) {
      const bool keep_reporting =
          base::FeatureList::IsEnabled(chrome::android::kUmaBackgroundSessions);
      metrics->OnAppEnterBackground(keep_reporting);
    }
    ukm::UkmService* ukm_service =
        g_browser_process->GetMetricsServicesManager()->GetUkmService();
    if (ukm_service)
      ukm_service->OnAppEnterBackground();

    AndroidSessionDurationsServiceFactory::OnAppEnterBackground(duration);

    // Note: Keep the line below after |metrics->OnAppEnterBackground()|.
    // Otherwise, |ProvideCurrentSessionData()| may report a small timeslice of
    // background session time toward the previous log.
    session_time_tracker_.BeginBackgroundSession();
  }
}

void UmaSessionStats::ProvideCurrentSessionData() {
  base::UmaHistogramBoolean("Session.IsActive", active_session_count_ != 0);

  // We record Session.Background.TotalDuration here to ensure each UMA log
  // containing a background session contains this histogram.
  session_time_tracker_.AccumulateBackgroundSessionTime();
  session_time_tracker_.ReportBackgroundSessionTime();
}

// static
UmaSessionStats* UmaSessionStats::GetInstance() {
  static base::NoDestructor<UmaSessionStats> instance;
  return instance.get();
}

// static
bool UmaSessionStats::HasVisibleActivity() {
  return Java_UmaSessionStats_hasVisibleActivity(
      base::android::AttachCurrentThread());
}

// Called on startup. If there is an activity, do nothing because a foreground
// session will be created naturally. Otherwise, begin recording a background
// session.
// static
void UmaSessionStats::OnStartup() {
  if (!UmaSessionStats::HasVisibleActivity()) {
    GetInstance()->session_time_tracker_.BeginBackgroundSession();
  }
}

// static
void UmaSessionStats::RegisterSyntheticFieldTrial(
    const std::string& trial_name,
    const std::string& group_name,
    variations::SyntheticTrialAnnotationMode annotation_mode) {
  ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
      trial_name, group_name, annotation_mode);
}

// static
bool UmaSessionStats::IsBackgroundSessionStartForTesting() {
  return !GetInstance()
              ->session_time_tracker_.background_session_start_time()
              .is_null();
}

void UmaSessionStats::EmitAndResetCounters() {
  std::optional<int> on_postcreate_counter =
      android::shared_preferences::GetAndClearInt(
          "Chrome.UMA.OnPostCreateCounter2");
  std::optional<int> on_resume_counter =
      android::shared_preferences::GetAndClearInt(
          "Chrome.UMA.OnResumeCounter2");
  int on_create_count = std::min(on_postcreate_counter.value_or(0), 3);
  int on_resume_count = std::min(on_resume_counter.value_or(0), 3);
  ChromeActivityCounter count_code =
      static_cast<ChromeActivityCounter>(4 * on_create_count + on_resume_count);
  UMA_HISTOGRAM_ENUMERATION("UMA.AndroidPreNative.ChromeActivityCounter2",
                            count_code);
}

void UmaSessionStats::SessionTimeTracker::AccumulateBackgroundSessionTime() {
  // No time spent in background since the last call to
  // |AccumulateBackgroundSessionTime()|.
  if (background_session_start_time_.is_null())
    return;

  base::TimeTicks now = base::TimeTicks::Now();
  base::TimeDelta duration = now - background_session_start_time_;
  background_session_accumulated_time_ += duration;

  background_session_start_time_ = now;
}

void UmaSessionStats::SessionTimeTracker::ReportBackgroundSessionTime() {
  if (background_session_accumulated_time_.is_zero())
    return;

  // This histogram is used in analysis to determine if an uploaded log
  // represents background activity. For this reason, this histogram may be
  // recorded more than once per 'background session'.
  UMA_HISTOGRAM_CUSTOM_TIMES("Session.Background.TotalDuration",
                             background_session_accumulated_time_,
                             base::Milliseconds(1), base::Hours(24), 50);
  background_session_accumulated_time_ = base::TimeDelta();
}

bool UmaSessionStats::SessionTimeTracker::BeginForegroundSession() {
  // Emit onPostCreate & onResume counters. This is done early in the session
  // to ensure that these are captured even if the session is not ended
  // cleanly.
  UmaSessionStats::EmitAndResetCounters();
  AccumulateBackgroundSessionTime();
  background_session_start_time_ = {};
  session_start_time_ = base::TimeTicks::Now();
  return !background_session_accumulated_time_.is_zero();
}

base::TimeDelta UmaSessionStats::SessionTimeTracker::EndForegroundSession() {
  base::TimeDelta duration = base::TimeTicks::Now() - session_start_time_;

  // Note: This metric is recorded separately on desktop in
  // DesktopSessionDurationTracker::EndSession.
  UMA_HISTOGRAM_LONG_TIMES("Session.TotalDuration", duration);
  UMA_HISTOGRAM_CUSTOM_TIMES("Session.TotalDurationMax1Day", duration,
                             base::Milliseconds(1), base::Hours(24), 50);
  return duration;
}

void UmaSessionStats::SessionTimeTracker::BeginBackgroundSession() {
  background_session_start_time_ = base::TimeTicks::Now();
}

// Updates metrics reporting state managed by native code. This should only be
// called when consent is changing, and UpdateMetricsServiceState() should be
// called immediately after for metrics services to be started or stopped as
// needed. This is enforced by UmaSessionStats.changeMetricsReportingConsent on
// the Java side.
static void JNI_UmaSessionStats_ChangeMetricsReportingConsent(
    JNIEnv*,
    jboolean consent,
    jint called_from) {
  UpdateMetricsPrefsOnPermissionChange(
      consent, static_cast<ChangeMetricsReportingStateCalledFrom>(called_from));

  // This function ensures a consent file in the data directory is either
  // created, or deleted, depending on consent. Starting up metrics services
  // will ensure that the consent file contains the ClientID. The ID is passed
  // to the renderer for crash reporting when things go wrong.
  GoogleUpdateSettings::CollectStatsConsentTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(
          base::IgnoreResult(GoogleUpdateSettings::SetCollectStatsConsent),
          consent));
}

// Initialize the local consent bool variable to false. Used only for testing.
static void JNI_UmaSessionStats_InitMetricsAndCrashReportingForTesting(
    JNIEnv*) {
  DCHECK(g_browser_process);

  g_metrics_consent_for_testing = false;
  ChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(
      &g_metrics_consent_for_testing);
}

// Clears the boolean consent pointer for ChromeMetricsServiceAccessor to
// original setting. Used only for testing.
static void JNI_UmaSessionStats_UnsetMetricsAndCrashReportingForTesting(
    JNIEnv*) {
  DCHECK(g_browser_process);

  g_metrics_consent_for_testing = false;
  ChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(nullptr);
}

// Updates the metrics consent bit to |consent|. This is separate from
// InitMetricsAndCrashReportingForTesting as the Set isn't meant to be used
// repeatedly. Used only for testing.
static void JNI_UmaSessionStats_UpdateMetricsAndCrashReportingForTesting(
    JNIEnv*,
    jboolean consent) {
  DCHECK(g_browser_process);

  g_metrics_consent_for_testing = consent;
  g_browser_process->GetMetricsServicesManager()->UpdateUploadPermissions(true);
}

// Starts/stops the MetricsService based on existing consent and upload
// preferences.
// There are three possible states:
// * Logs are being recorded and being uploaded to the server.
// * Logs are being recorded, but not being uploaded to the server.
//   This happens when we've got permission to upload on Wi-Fi but we're on a
//   mobile connection (for example).
// * Logs are neither being recorded or uploaded.
// If logs aren't being recorded, then |may_upload| is ignored.
//
// This can be called at any time when consent hasn't changed, such as
// connection type change, or start up. If consent has changed, then
// ChangeMetricsReportingConsent() should be called first.
static void JNI_UmaSessionStats_UpdateMetricsServiceState(
    JNIEnv*,
    jboolean may_upload) {
  // This will also apply the consent state, taken from Chrome Local State
  // prefs.
  g_browser_process->GetMetricsServicesManager()->UpdateUploadPermissions(
      may_upload);
}

static void JNI_UmaSessionStats_RegisterExternalExperiment(
    JNIEnv* env,
    const JavaParamRef<jintArray>& jexperiment_ids,
    jboolean override_existing_ids) {
  std::vector<int> experiment_ids;
  // A null |jexperiment_ids| is the same as an empty list.
  if (jexperiment_ids) {
    base::android::JavaIntArrayToIntVector(env, jexperiment_ids,
                                           &experiment_ids);
  }

  auto override_mode =
      override_existing_ids
          ? variations::SyntheticTrialRegistry::kOverrideExistingIds
          : variations::SyntheticTrialRegistry::kDoNotOverrideExistingIds;

  g_browser_process->metrics_service()
      ->GetSyntheticTrialRegistry()
      ->RegisterExternalExperiments(experiment_ids, override_mode);
}

static void JNI_UmaSessionStats_RegisterSyntheticFieldTrial(
    JNIEnv* env,
    const JavaParamRef<jstring>& jtrial_name,
    const JavaParamRef<jstring>& jgroup_name,
    int annotation_mode) {
  std::string trial_name(ConvertJavaStringToUTF8(env, jtrial_name));
  std::string group_name(ConvertJavaStringToUTF8(env, jgroup_name));
  UmaSessionStats::RegisterSyntheticFieldTrial(
      trial_name, group_name,
      static_cast<variations::SyntheticTrialAnnotationMode>(annotation_mode));
}

static void JNI_UmaSessionStats_RecordTabCountPerLoad(
    JNIEnv*,
    jint num_tabs) {
  // Record how many tabs total are open.
  UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.TabCountPerLoad", num_tabs, 1, 200, 50);
}

static void JNI_UmaSessionStats_RecordPageLoaded(
    JNIEnv*,
    jboolean is_desktop_user_agent) {
  // Should be called whenever a page has been loaded.
  base::RecordAction(UserMetricsAction("MobilePageLoaded"));
  if (is_desktop_user_agent) {
    base::RecordAction(UserMetricsAction("MobilePageLoadedDesktopUserAgent"));
  }
}

static void JNI_UmaSessionStats_RecordPageLoadedWithAccessory(JNIEnv*) {
  base::RecordAction(UserMetricsAction("MobilePageLoadedWithAccessory"));
}

static void JNI_UmaSessionStats_RecordPageLoadedWithKeyboard(JNIEnv*) {
  base::RecordAction(UserMetricsAction("MobilePageLoadedWithKeyboard"));
}

static void JNI_UmaSessionStats_RecordPageLoadedWithMouse(JNIEnv*) {
  base::RecordAction(UserMetricsAction("MobilePageLoadedWithMouse"));
}

static void JNI_UmaSessionStats_RecordPageLoadedWithToEdge(JNIEnv*) {
  base::RecordAction(UserMetricsAction("MobilePageLoadedWithToEdge"));
}

static jlong JNI_UmaSessionStats_Init(JNIEnv* env) {
  // We should have only one UmaSessionStats instance.
  return reinterpret_cast<intptr_t>(UmaSessionStats::GetInstance());
}