| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/metrics/demo_session_metrics_recorder.h" |
| |
| #include <iostream> |
| #include <string> |
| #include <utility> |
| |
| #include "ash/constants/web_app_id_constants.h" |
| #include "ash/public/cpp/app_types_util.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/shelf/shelf_window_watcher.h" |
| #include "ash/shell.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/scoped_multi_source_observation.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "chromeos/ui/base/app_types.h" |
| #include "chromeos/ui/base/window_properties.h" |
| #include "components/app_constants/constants.h" |
| #include "extensions/common/constants.h" |
| #include "ui/aura/client/window_types.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/wm/core/focus_controller.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace ash { |
| namespace { |
| |
| using DemoModeApp = DemoSessionMetricsRecorder::DemoModeApp; |
| |
| using ExitSessionFrom = DemoSessionMetricsRecorder::ExitSessionFrom; |
| |
| using SessionType = DemoSessionMetricsRecorder::SessionType; |
| |
| DemoSessionMetricsRecorder* g_demo_session_metrics_recorder = nullptr; |
| |
| // It is reset to this default value every session, and DemoLoginController will |
| // set it to the other session type if needed. |
| // |
| // We keep it as a global variable instead of owning by |
| // DemoSessionMetricsRecorder, because DemoSessionMetricsRecorder is not |
| // initiated yet when DemoLoginController is setting its value before entering |
| // the session. |
| SessionType current_session_type = SessionType::kClassicMGS; |
| |
| // How often to sample. |
| constexpr auto kSamplePeriod = base::Seconds(1); |
| |
| // Minimum app usage time. |
| constexpr base::TimeDelta kMinimumAppUsageTime = base::Seconds(1); |
| |
| // Redefining chromeos::preinstalled_web_apps::kHelpAppId as ash can't depend on |
| // chrome. |
| constexpr char kHelpAppId[] = "nbljnnecbjbmifnoehiemkgefbnpoeak"; |
| |
| constexpr char kDemoModeSignedInShopperDwellTime[] = |
| "DemoMode.SignedIn.Shopper.DwellTime"; |
| constexpr char kDemoModeSignedInMGSFallbackShopperDwellTime[] = |
| "DemoMode.SignedIn.MGSFallback.Shopper.DwellTime"; |
| |
| constexpr char kSetupDemoAccountRequestResult[] = |
| "DemoMode.SignedIn.Request.SetupResult"; |
| constexpr char kCleanupDemoAccountRequestResult[] = |
| "DemoMode.SignedIn.Request.CleanupResult"; |
| constexpr char kAppUsageTimeHistogramPrefix[] = "DemoMode.AppUsageTime."; |
| |
| constexpr char kCloudPolicyConnectionTimeoutAction[] = |
| "DemoMode.CloudPolicyConnectionTimeout"; |
| |
| struct AppHistogramSuffix { |
| const DemoModeApp app_type; |
| const std::string name; |
| }; |
| |
| // Apps in Demo mode have the highest launched count. Note that |
| // `DemoModeApp::kOtherChromeApp` includes the demo mode SWA. Not recording this |
| // one until we exclude it from `DemoModeApp::kOtherChromeApp`. |
| const AppHistogramSuffix kAppsHistogramSuffix[] = { |
| {DemoModeApp::kGooglePhotos, "GooglePhoto"}, |
| {DemoModeApp::kStardewValley, "StardewValley"}, |
| {DemoModeApp::kMinecraft, "Minecraft"}, |
| {DemoModeApp::kPlayStore, "PlayStore"}, |
| {DemoModeApp::kOtherArcApp, "OtherArcApp"}, |
| {DemoModeApp::kBrowser, "Browser"}, |
| {DemoModeApp::kYoutubePwa, "YouTubePwa"}, |
| {DemoModeApp::kZoom, "Zoom"}, |
| {DemoModeApp::kGoogleDocsPwa, "GoogleDocs"}, |
| {DemoModeApp::kSumo, "Sumo"}, |
| {DemoModeApp::kBeFunky, "BeFunky"}, |
| {DemoModeApp::kSpotify, "Spotify"}, |
| {DemoModeApp::kFiles, "FilesManager"}, |
| {DemoModeApp::kGemini, "Gemini"}, |
| }; |
| |
| // How many periods to wait for user activity before discarding samples. |
| // This timeout is low because demo sessions tend to be very short. If we |
| // recorded samples for a full minute while the device is in between uses, we |
| // would bias our measurements toward whatever app was used last. |
| constexpr int kMaxPeriodsWithoutActivity = base::Seconds(15) / kSamplePeriod; |
| |
| // Maps a Chrome app ID to a DemoModeApp value for metrics. |
| DemoModeApp GetAppFromAppId(const std::string& app_id) { |
| // Each version of the Highlights app is bucketed into the same value. |
| if (app_id == extension_misc::kHighlightsAppId || |
| app_id == extension_misc::kNewHighlightsAppId || |
| app_id == extension_misc::kDemoModeSWA) { |
| return DemoModeApp::kHighlights; |
| } |
| |
| // Each version of the Screensaver app is bucketed into the same value. |
| if (app_id == extension_misc::kScreensaverAppId || |
| app_id == extension_misc::kNewAttractLoopAppId) { |
| return DemoModeApp::kScreensaver; |
| } |
| |
| if (app_id == app_constants::kChromeAppId) { |
| return DemoModeApp::kBrowser; |
| } |
| if (app_id == extension_misc::kFilesManagerAppId || |
| app_id == extension_misc::kFilesManagerSWAId) { |
| return DemoModeApp::kFiles; |
| } |
| if (app_id == extension_misc::kCalculatorAppId) { |
| return DemoModeApp::kCalculator; |
| } |
| if (app_id == extension_misc::kCalendarDemoAppId) { |
| return DemoModeApp::kCalendar; |
| } |
| if (app_id == extension_misc::kCameraAppId) { |
| return DemoModeApp::kCamera; |
| } |
| if (app_id == extension_misc::kGoogleDocsDemoAppId) { |
| return DemoModeApp::kGoogleDocsChromeApp; |
| } |
| if (app_id == extension_misc::kGoogleDocsPwaAppId || |
| app_id == ash::kGoogleDocsAppId) { |
| return DemoModeApp::kGoogleDocsPwa; |
| } |
| if (app_id == extension_misc::kGoogleMeetPwaAppId) { |
| return DemoModeApp::kGoogleMeetPwa; |
| } |
| if (app_id == extension_misc::kGoogleSheetsDemoAppId) { |
| return DemoModeApp::kGoogleSheetsChromeApp; |
| } |
| if (app_id == extension_misc::kGoogleSheetsPwaAppId) { |
| return DemoModeApp::kGoogleSheetsPwa; |
| } |
| if (app_id == extension_misc::kGoogleSlidesDemoAppId) { |
| return DemoModeApp::kGoogleSlidesChromeApp; |
| } |
| if (app_id == kHelpAppId) { |
| return DemoModeApp::kGetHelp; |
| } |
| if (app_id == extension_misc::kGoogleKeepAppId) { |
| return DemoModeApp::kGoogleKeepChromeApp; |
| } |
| if (app_id == extensions::kWebStoreAppId) { |
| return DemoModeApp::kWebStore; |
| } |
| if (app_id == extension_misc::kYoutubeAppId) { |
| return DemoModeApp::kYouTube; |
| } |
| if (app_id == extension_misc::kYoutubePwaAppId) { |
| return DemoModeApp::kYoutubePwa; |
| } |
| if (app_id == extension_misc::kSpotifyAppId) { |
| return DemoModeApp::kSpotify; |
| } |
| if (app_id == extension_misc::kBeFunkyAppId) { |
| return DemoModeApp::kBeFunky; |
| } |
| if (app_id == extension_misc::kClipchampAppId) { |
| return DemoModeApp::kClipchamp; |
| } |
| if (app_id == extension_misc::kGeForceNowAppId) { |
| return DemoModeApp::kGeForceNow; |
| } |
| if (app_id == extension_misc::kZoomAppId) { |
| return DemoModeApp::kZoom; |
| } |
| if (app_id == extension_misc::kSumoAppId) { |
| return DemoModeApp::kSumo; |
| } |
| if (app_id == extension_misc::kAdobeSparkAppId) { |
| return DemoModeApp::kAdobeSpark; |
| } |
| if (app_id == extension_misc::kGeminiAppId || |
| app_id == extension_misc::kGeminiAppByPolicyId) { |
| return DemoModeApp::kGemini; |
| } |
| |
| return DemoModeApp::kOtherChromeApp; |
| } |
| |
| const std::string GetAppHistogramSuffix(DemoModeApp app_type) { |
| const AppHistogramSuffix* suffix = std::ranges::find( |
| kAppsHistogramSuffix, app_type, &AppHistogramSuffix::app_type); |
| if (suffix == std::end(kAppsHistogramSuffix)) { |
| return std::string(); |
| } |
| return suffix->name; |
| } |
| |
| // Maps an ARC++ package name to a DemoModeApp value for metrics. |
| DemoModeApp GetAppFromPackageName(const std::string& package_name) { |
| // Google apps. |
| if (package_name == "com.google.Photos" || |
| package_name == "com.google.android.apps.photos") { |
| return DemoModeApp::kGooglePhotos; |
| } |
| if (package_name == "com.google.Sheets" || |
| package_name == "com.google.android.apps.docs.editors.sheets") { |
| return DemoModeApp::kGoogleSheetsAndroidApp; |
| } |
| if (package_name == "com.google.Slides" || |
| package_name == "com.google.android.apps.docs.editors.slides") { |
| return DemoModeApp::kGoogleSlidesAndroidApp; |
| } |
| if (package_name == "com.google.android.keep") { |
| return DemoModeApp::kGoogleKeepAndroidApp; |
| } |
| if (package_name == "com.android.vending") { |
| return DemoModeApp::kPlayStore; |
| } |
| |
| // Third-party apps. |
| if (package_name == "com.gameloft.android.ANMP.GloftA8HMD") { |
| return DemoModeApp::kAsphalt8; |
| } |
| if (package_name == "com.gameloft.android.ANMP.GloftA9HM" || |
| package_name == "com.gameloft.android.ANMP.GloftA9HMD") { |
| return DemoModeApp::kAsphalt9; |
| } |
| if (package_name == "com.chucklefish.stardewvalley" || |
| package_name == "com.chucklefish.stardewvalleydemo") { |
| return DemoModeApp::kStardewValley; |
| } |
| if (package_name == "com.nexstreaming.app.kinemasterfree" || // nocheck |
| package_name == |
| "com.nexstreaming.app.kinemasterfree.demo.chromebook") { // nocheck |
| return DemoModeApp::kKinemaster; // nocheck |
| } |
| if (package_name == "com.pixlr.express" || |
| package_name == "com.pixlr.express.chromebook.demo") { |
| return DemoModeApp::kPixlr; |
| } |
| if (package_name == "com.brakefield.painter") { |
| return DemoModeApp::kInfinitePainter; |
| } |
| if (package_name == "com.myscript.nebo.demo") { |
| return DemoModeApp::kMyScriptNebo; |
| } |
| if (package_name == "com.steadfastinnovation.android.projectpapyrus") { |
| return DemoModeApp::kSquid; |
| } |
| if (package_name == "com.autodesk.autocadws.demo") { |
| return DemoModeApp::kAutoCAD; |
| } |
| if (package_name == "com.mojang.minecrafttrialpe") { |
| return DemoModeApp::kMinecraft; |
| } |
| |
| return DemoModeApp::kOtherArcApp; |
| } |
| |
| chromeos::AppType GetAppType(const aura::Window* window) { |
| return window->GetProperty(chromeos::kAppTypeKey); |
| } |
| |
| const std::string* GetArcPackageName(const aura::Window* window) { |
| DCHECK(IsArcWindow(window)); |
| return window->GetProperty(kArcPackageNameKey); |
| } |
| |
| bool CanGetAppFromWindow(const aura::Window* window) { |
| // For ARC apps we can only get the App if the package |
| // name is not null. |
| if (IsArcWindow(window)) { |
| return GetArcPackageName(window) != nullptr; |
| } |
| // We can always get the App for non-ARC windows. |
| return true; |
| } |
| |
| const ShelfID GetShelfID(const aura::Window* window) { |
| return ShelfID::Deserialize(window->GetProperty(kShelfIDKey)); |
| } |
| |
| // Maps the app-like thing in |window| to a DemoModeApp value for metrics. |
| DemoModeApp GetAppFromWindow(const aura::Window* window) { |
| DCHECK(CanGetAppFromWindow(window)); |
| |
| chromeos::AppType app_type = GetAppType(window); |
| if (app_type == chromeos::AppType::ARC_APP) { |
| // The ShelfID app id isn't used to identify ARC++ apps since it's a hash |
| // of both the package name and the activity. |
| const std::string* package_name = GetArcPackageName(window); |
| return GetAppFromPackageName(*package_name); |
| } |
| |
| std::string app_id = GetShelfID(window).app_id; |
| |
| // The Chrome "app" in the shelf is just the browser. |
| if (app_id == app_constants::kChromeAppId) { |
| return DemoModeApp::kBrowser; |
| } |
| |
| // If the window is the "browser" type, having an app ID other than the |
| // default indicates a hosted/bookmark app. |
| if (app_type == chromeos::AppType::CHROME_APP || |
| (app_type == chromeos::AppType::BROWSER && !app_id.empty())) { |
| return GetAppFromAppId(app_id); |
| } |
| |
| if (app_type == chromeos::AppType::BROWSER) { |
| return DemoModeApp::kBrowser; |
| } |
| return DemoModeApp::kOtherWindow; |
| } |
| |
| // Identical to UmaHistogramLongTimes100, but reports times with second |
| // granularity instead of millisecond granularity. |
| // This significantly improves the bucketing if millisecond granularity is |
| // not required - 90/100 buckets are greater than 10 seconds, compared to |
| // 43/100 buckets using millisecond accuracy with min=1ms, or |
| // 72/100 buckets using millisecond accuracy with min=1000ms. |
| void ReportHistogramLongSecondsTimes100(const char* name, |
| base::TimeDelta sample) { |
| // We use a max of 1 hour = 60 * 60 secs. |
| base::UmaHistogramCustomCounts(name, |
| base::saturated_cast<int>(sample.InSeconds()), |
| /*min=*/1, /*max=*/60 * 60, /*buckets=*/100); |
| } |
| |
| std::string GetExitSessionActionName(ExitSessionFrom recorded_from, |
| bool for_signed_in_session) { |
| if (for_signed_in_session) { |
| switch (recorded_from) { |
| case ExitSessionFrom::kShelf: |
| return "DemoMode.SignedIn.ExitFromShelf"; |
| case ExitSessionFrom::kSystemTray: |
| return "DemoMode.SignedIn.ExitFromSystemTray"; |
| case ExitSessionFrom::kSystemTrayPowerButton: |
| return "DemoMode.SignedIn.ExitFromSystemTrayPowerButton"; |
| default: |
| NOTREACHED(); |
| } |
| } else { |
| switch (recorded_from) { |
| case ExitSessionFrom::kShelf: |
| return "DemoMode.ExitFromShelf"; |
| case ExitSessionFrom::kSystemTray: |
| return "DemoMode.ExitFromSystemTray"; |
| case ExitSessionFrom::kSystemTrayPowerButton: |
| return "DemoMode.ExitFromSystemTrayPowerButton"; |
| default: |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| } // namespace |
| |
| // Observes for changes in a window's ArcPackageName property for the purpose of |
| // logging of active app samples. |
| class DemoSessionMetricsRecorder::ActiveAppArcPackageNameObserver |
| : public aura::WindowObserver { |
| public: |
| explicit ActiveAppArcPackageNameObserver( |
| DemoSessionMetricsRecorder* metrics_recorder) |
| : metrics_recorder_(metrics_recorder) {} |
| |
| ActiveAppArcPackageNameObserver(const ActiveAppArcPackageNameObserver&) = |
| delete; |
| ActiveAppArcPackageNameObserver& operator=( |
| const ActiveAppArcPackageNameObserver&) = delete; |
| |
| // aura::WindowObserver |
| void OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) override { |
| if (key != kArcPackageNameKey) { |
| return; |
| } |
| |
| const std::string* package_name = GetArcPackageName(window); |
| |
| if (package_name) { |
| metrics_recorder_->RecordActiveAppSample( |
| GetAppFromPackageName(*package_name)); |
| } else { |
| VLOG(1) << "Got null ARC package name"; |
| } |
| |
| scoped_observations_.RemoveObservation(window); |
| } |
| |
| void OnWindowDestroyed(aura::Window* window) override { |
| if (scoped_observations_.IsObservingSource(window)) { |
| scoped_observations_.RemoveObservation(window); |
| } |
| } |
| |
| void ObserveWindow(aura::Window* window) { |
| if (scoped_observations_.IsObservingSource(window)) { |
| return; |
| } |
| scoped_observations_.AddObservation(window); |
| } |
| |
| private: |
| raw_ptr<DemoSessionMetricsRecorder> metrics_recorder_; |
| base::ScopedMultiSourceObservation<aura::Window, aura::WindowObserver> |
| scoped_observations_{this}; |
| }; |
| |
| // Observes changes in a window's ArcPackageName property for the purpose of |
| // logging of unique launches of ARC apps. |
| // TODO(crbug.com/393457908): Remove this. |
| // `UniqueAppsLaunchedArcPackageNameObserver` is a singleton and cannot observe |
| // multiple arc package launch at the same time. |
| class DemoSessionMetricsRecorder::UniqueAppsLaunchedArcPackageNameObserver |
| : public aura::WindowObserver { |
| public: |
| explicit UniqueAppsLaunchedArcPackageNameObserver( |
| DemoSessionMetricsRecorder* metrics_recorder) |
| : metrics_recorder_(metrics_recorder) {} |
| |
| UniqueAppsLaunchedArcPackageNameObserver( |
| const UniqueAppsLaunchedArcPackageNameObserver&) = delete; |
| UniqueAppsLaunchedArcPackageNameObserver& operator=( |
| const UniqueAppsLaunchedArcPackageNameObserver&) = delete; |
| |
| // aura::WindowObserver |
| void OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) override { |
| if (key != kArcPackageNameKey) { |
| return; |
| } |
| |
| const std::string* package_name = GetArcPackageName(window); |
| |
| if (package_name) { |
| metrics_recorder_->RecordAppLaunch(*package_name, |
| chromeos::AppType::ARC_APP); |
| } else { |
| VLOG(1) << "Got null ARC package name"; |
| } |
| |
| DCHECK(scoped_observation_.IsObservingSource(window)); |
| scoped_observation_.Reset(); |
| } |
| |
| void OnWindowDestroyed(aura::Window* window) override { |
| DCHECK(scoped_observation_.IsObservingSource(window)); |
| scoped_observation_.Reset(); |
| } |
| |
| void ObserveWindow(aura::Window* window) { |
| scoped_observation_.Reset(); |
| scoped_observation_.Observe(window); |
| } |
| |
| private: |
| raw_ptr<DemoSessionMetricsRecorder> metrics_recorder_; |
| base::ScopedObservation<aura::Window, aura::WindowObserver> |
| scoped_observation_{this}; |
| }; |
| |
| // static |
| void DemoSessionMetricsRecorder::RecordExitSessionAction( |
| ExitSessionFrom recorded_from) { |
| // Record generic exit demo session user action regardless of the signed-in |
| // status. |
| const std::string action_name = |
| GetExitSessionActionName(recorded_from, false); |
| base::RecordAction(base::UserMetricsAction(action_name.c_str())); |
| |
| if (current_session_type == SessionType::kSignedInDemoSession) { |
| // Record signed-in session related action. |
| const std::string signed_in_action_name = |
| GetExitSessionActionName(recorded_from, true); |
| base::RecordAction(base::UserMetricsAction(signed_in_action_name.c_str())); |
| } |
| } |
| |
| // static |
| DemoSessionMetricsRecorder* DemoSessionMetricsRecorder::Get() { |
| return g_demo_session_metrics_recorder; |
| } |
| |
| // static |
| void DemoSessionMetricsRecorder::ReportDemoAccountSetupResult( |
| DemoAccountRequestResultCode result_code) { |
| base::UmaHistogramEnumeration(kSetupDemoAccountRequestResult, result_code); |
| } |
| |
| // static |
| void DemoSessionMetricsRecorder::ReportDemoAccountCleanupResult( |
| DemoAccountRequestResultCode result_code) { |
| base::UmaHistogramEnumeration(kCleanupDemoAccountRequestResult, result_code); |
| } |
| |
| // static |
| void DemoSessionMetricsRecorder::SetCurrentSessionType( |
| SessionType session_type) { |
| current_session_type = session_type; |
| } |
| |
| // static |
| SessionType DemoSessionMetricsRecorder::GetCurrentSessionTypeForTesting() { |
| return current_session_type; |
| } |
| |
| // static |
| void DemoSessionMetricsRecorder::RecordCloudPolicyConnectionTimeout() { |
| base::RecordAction( |
| base::UserMetricsAction(kCloudPolicyConnectionTimeoutAction)); |
| } |
| |
| DemoSessionMetricsRecorder::DemoSessionMetricsRecorder( |
| std::unique_ptr<base::RepeatingTimer> timer) |
| : timer_(std::move(timer)), |
| unique_apps_arc_package_name_observer_( |
| std::make_unique<UniqueAppsLaunchedArcPackageNameObserver>(this)), |
| active_app_arc_package_name_observer_( |
| std::make_unique<ActiveAppArcPackageNameObserver>(this)) { |
| CHECK(!g_demo_session_metrics_recorder); |
| g_demo_session_metrics_recorder = this; |
| |
| // Outside of tests, use a normal repeating timer. |
| if (!timer_.get()) { |
| timer_ = std::make_unique<base::RepeatingTimer>(); |
| } |
| |
| StartRecording(); |
| observation_.Observe(ui::UserActivityDetector::Get()); |
| |
| // Subscribe to window activation updates. Even though this gets us |
| // notifications for all window activations, we ignore the ARC |
| // notifications because they don't contain the app_id. We handle |
| // accounting for ARC windows with OnTaskCreated. |
| if (Shell::Get()->GetPrimaryRootWindow()) { |
| activation_client_ = Shell::Get()->focus_controller(); |
| activation_client_->AddObserver(this); |
| } |
| } |
| |
| DemoSessionMetricsRecorder::~DemoSessionMetricsRecorder() { |
| // TODO(crbug.com/393457908): Fix under reported metric record during |
| // shutdown. |
| |
| // Report any remaining stored samples on exit. (If the user went idle, there |
| // won't be any.) |
| ReportSamples(); |
| |
| ReportShopperSessionDwellTime(); |
| |
| ReportDwellTime(); |
| |
| ReportUserClickesAndPresses(); |
| |
| // Unsubscribe from window activation events. |
| activation_client_->RemoveObserver(this); |
| |
| ReportUniqueAppsLaunched(); |
| |
| g_demo_session_metrics_recorder = nullptr; |
| } |
| |
| void DemoSessionMetricsRecorder::RecordAppLaunch(const std::string& id, |
| chromeos::AppType app_type) { |
| if (!ShouldRecordAppLaunch(id)) { |
| return; |
| } |
| DemoModeApp app; |
| if (app_type == chromeos::AppType::ARC_APP) { |
| app = GetAppFromPackageName(id); |
| } else { |
| app = GetAppFromAppId(id); |
| } |
| |
| if (!unique_apps_launched_.contains(id)) { |
| unique_apps_launched_.insert(id); |
| // Only log each app launch once. This is determined by |
| // checking the package_name instead of the DemoApp enum, |
| // because the DemoApp enum collapses unknown apps into |
| // a single enum. |
| UMA_HISTOGRAM_ENUMERATION("DemoMode.AppLaunched", app); |
| } |
| } |
| |
| // Indicates whether the specified app_id should be recorded for |
| // the unique-apps-launched stat. |
| bool DemoSessionMetricsRecorder::ShouldRecordAppLaunch( |
| const std::string& app_id) { |
| return unique_apps_launched_recording_enabled_ && |
| GetAppFromAppId(app_id) != DemoModeApp::kHighlights && |
| GetAppFromAppId(app_id) != DemoModeApp::kScreensaver; |
| } |
| |
| void DemoSessionMetricsRecorder::OnWindowActivated(ActivationReason reason, |
| aura::Window* gained_active, |
| aura::Window* lost_active) { |
| if (!gained_active) { |
| return; |
| } |
| |
| // Don't count popup windows. |
| if (gained_active->GetType() != aura::client::WINDOW_TYPE_NORMAL) { |
| return; |
| } |
| |
| chromeos::AppType app_type = GetAppType(gained_active); |
| |
| std::string app_id; |
| if (app_type == chromeos::AppType::ARC_APP) { |
| const std::string* package_name = GetArcPackageName(gained_active); |
| |
| if (!package_name) { |
| // The package name property for the window has not been set yet. |
| // Listen for changes to the window properties so we can |
| // be informed when the package name gets set. |
| if (!gained_active->HasObserver( |
| unique_apps_arc_package_name_observer_.get())) { |
| unique_apps_arc_package_name_observer_->ObserveWindow(gained_active); |
| } |
| return; |
| } |
| app_id = *package_name; |
| } else { |
| // This is a non-ARC window, so we just get the shelf ID, which should |
| // be unique per app. |
| app_id = GetShelfID(gained_active).app_id; |
| } |
| |
| // Some app_ids are empty, i.e the "You will be signed out |
| // in X seconds" modal dialog in Demo Mode, so skip those. |
| if (app_id.empty()) { |
| return; |
| } |
| |
| RecordAppLaunch(app_id, app_type); |
| } |
| |
| void DemoSessionMetricsRecorder::OnUserActivity(const ui::Event* event) { |
| // Record the first and last user activities upon observing them. |
| base::TimeTicks now = base::TimeTicks::Now(); |
| if (first_user_activity_.is_null()) { |
| first_user_activity_ = now; |
| } |
| if (shopper_session_first_user_activity_.is_null()) { |
| shopper_session_first_user_activity_ = now; |
| } |
| last_user_activity_ = now; |
| |
| // Report samples recorded since the last activity. |
| ReportSamples(); |
| |
| // Restart the timer if the device has been idle. |
| if (!timer_->IsRunning()) { |
| StartRecording(); |
| } |
| periods_since_activity_ = 0; |
| } |
| |
| void DemoSessionMetricsRecorder::OnMouseEvent(ui::MouseEvent* event) { |
| // If event type is mouse/trackpad clicking, increase the metric by one. |
| if (event->type() == ui::EventType::kMousePressed) { |
| user_clicks_and_presses_++; |
| } |
| } |
| |
| void DemoSessionMetricsRecorder::OnTouchEvent(ui::TouchEvent* event) { |
| // If event type is screen pressing, increase the metric by one. |
| if (event->type() == ui::EventType::kTouchPressed) { |
| user_clicks_and_presses_++; |
| } |
| } |
| |
| void DemoSessionMetricsRecorder::ReportShopperSessionDwellTime() { |
| if (shopper_session_first_user_activity_.is_null()) { |
| return; |
| } |
| if (current_session_type == SessionType::kSignedInDemoSession || |
| current_session_type == SessionType::kFallbackMGS) { |
| DCHECK(!last_user_activity_.is_null()); |
| DCHECK_LE(shopper_session_first_user_activity_, last_user_activity_); |
| |
| base::TimeDelta dwell_time = |
| last_user_activity_ - shopper_session_first_user_activity_; |
| ReportHistogramLongSecondsTimes100( |
| current_session_type == SessionType::kSignedInDemoSession |
| ? kDemoModeSignedInShopperDwellTime |
| : kDemoModeSignedInMGSFallbackShopperDwellTime, |
| dwell_time); |
| } |
| shopper_session_first_user_activity_ = base::TimeTicks(); |
| } |
| |
| void DemoSessionMetricsRecorder::OnAppCreation( |
| const std::string& app_id_or_package, |
| const bool is_arc_app) { |
| const DemoModeApp app = is_arc_app ? GetAppFromPackageName(app_id_or_package) |
| : GetAppFromAppId(app_id_or_package); |
| if (GetAppHistogramSuffix(app).empty()) { |
| return; |
| } |
| apps_start_time_[app] = base::TimeTicks::Now(); |
| } |
| |
| void DemoSessionMetricsRecorder::OnAppDestruction( |
| const std::string& app_id_or_package, |
| const bool is_arc_app) { |
| const DemoModeApp app = is_arc_app ? GetAppFromPackageName(app_id_or_package) |
| : GetAppFromAppId(app_id_or_package); |
| if (!apps_start_time_.contains(app)) { |
| return; |
| } |
| |
| const auto duration = base::TimeTicks::Now() - apps_start_time_[app]; |
| apps_start_time_.erase(app); |
| |
| // Some Arc app gets quickly created and destructed after `OnAppDestruction` |
| // get called. Ignore reporting if the duration is too short. |
| if (duration < kMinimumAppUsageTime) { |
| return; |
| } |
| |
| const std::string histogram_suffix = GetAppHistogramSuffix(app); |
| ReportHistogramLongSecondsTimes100( |
| base::StrCat({kAppUsageTimeHistogramPrefix, histogram_suffix}).c_str(), |
| duration); |
| } |
| |
| void DemoSessionMetricsRecorder::StartRecording() { |
| unique_apps_launched_recording_enabled_ = true; |
| timer_->Start(FROM_HERE, kSamplePeriod, this, |
| &DemoSessionMetricsRecorder::TakeSampleOrPause); |
| } |
| |
| void DemoSessionMetricsRecorder::RecordActiveAppSample(DemoModeApp app) { |
| unreported_samples_.push_back(app); |
| } |
| |
| void DemoSessionMetricsRecorder::TakeSampleOrPause() { |
| // After enough inactive time, assume the user left. |
| if (++periods_since_activity_ > kMaxPeriodsWithoutActivity) { |
| // These samples were collected since the last user activity. |
| unreported_samples_.clear(); |
| timer_->Stop(); |
| return; |
| } |
| |
| aura::Window* window = Shell::Get()->activation_client()->GetActiveWindow(); |
| if (!window) { |
| return; |
| } |
| |
| // If there is no ARC package name available, set up a listener |
| // to be informed when it is available. |
| if (IsArcWindow(window) && !CanGetAppFromWindow(window)) { |
| active_app_arc_package_name_observer_->ObserveWindow(window); |
| return; |
| } |
| |
| DemoModeApp app = window->GetType() == aura::client::WINDOW_TYPE_NORMAL |
| ? GetAppFromWindow(window) |
| : DemoModeApp::kOtherWindow; |
| RecordActiveAppSample(app); |
| } |
| |
| void DemoSessionMetricsRecorder::ReportSamples() { |
| for (DemoModeApp app : unreported_samples_) { |
| UMA_HISTOGRAM_ENUMERATION("DemoMode.ActiveApp", app); |
| } |
| unreported_samples_.clear(); |
| } |
| |
| void DemoSessionMetricsRecorder::ReportUniqueAppsLaunched() { |
| if (unique_apps_launched_recording_enabled_) { |
| UMA_HISTOGRAM_COUNTS_100("DemoMode.UniqueAppsLaunched", |
| unique_apps_launched_.size()); |
| } |
| unique_apps_launched_.clear(); |
| } |
| |
| void DemoSessionMetricsRecorder::ReportDwellTime() { |
| if (!first_user_activity_.is_null()) { |
| DCHECK(!last_user_activity_.is_null()); |
| DCHECK_LE(first_user_activity_, last_user_activity_); |
| |
| base::TimeDelta dwell_time = last_user_activity_ - first_user_activity_; |
| ReportHistogramLongSecondsTimes100("DemoMode.DwellTime", dwell_time); |
| } |
| first_user_activity_ = base::TimeTicks(); |
| last_user_activity_ = base::TimeTicks(); |
| } |
| |
| void DemoSessionMetricsRecorder::ReportUserClickesAndPresses() { |
| UMA_HISTOGRAM_COUNTS_1000( |
| DemoSessionMetricsRecorder::kUserClicksAndPressesMetric, |
| user_clicks_and_presses_); |
| } |
| |
| } // namespace ash |