| // 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) { |
| 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) { |
| --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(); |
| } |
| |
| // 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, |
| std::string& trial_name, |
| std::string& group_name, |
| int annotation_mode) { |
| 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()); |
| } |