| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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/public/cpp/app_types.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/shelf/shelf_window_watcher.h" |
| #include "ash/shell.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/scoped_observer.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "extensions/common/constants.h" |
| #include "ui/aura/client/aura_constants.h" |
| #include "ui/aura/client/window_types.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/base/user_activity/user_activity_detector.h" |
| #include "ui/wm/core/focus_controller.h" |
| #include "ui/wm/public/activation_client.h" |
| |
| namespace ash { |
| namespace { |
| |
| using DemoModeApp = DemoSessionMetricsRecorder::DemoModeApp; |
| |
| // How often to sample. |
| constexpr auto kSamplePeriod = base::TimeDelta::FromSeconds(1); |
| |
| // 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::TimeDelta::FromSeconds(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::kHighlightsAlt1AppId || |
| app_id == extension_misc::kHighlightsAlt2AppId) { |
| 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::kScreensaverAlt1AppId || |
| app_id == extension_misc::kScreensaverAlt2AppId) { |
| return DemoModeApp::kScreensaver; |
| } |
| |
| if (app_id == extension_misc::kCameraAppId) |
| return DemoModeApp::kCamera; |
| if (app_id == extension_misc::kChromeAppId) |
| return DemoModeApp::kBrowser; |
| if (app_id == extension_misc::kFilesManagerAppId) |
| return DemoModeApp::kFiles; |
| if (app_id == extension_misc::kGeniusAppId) |
| return DemoModeApp::kGetHelp; |
| if (app_id == extension_misc::kGoogleKeepAppId) |
| return DemoModeApp::kGoogleKeep; |
| if (app_id == extensions::kWebStoreAppId) |
| return DemoModeApp::kWebStore; |
| if (app_id == extension_misc::kYoutubeAppId) |
| return DemoModeApp::kYouTube; |
| return DemoModeApp::kOtherChromeApp; |
| } |
| |
| // 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") |
| return DemoModeApp::kGooglePhotos; |
| if (package_name == "com.google.Sheets") |
| return DemoModeApp::kGoogleSheets; |
| if (package_name == "com.google.Slides") |
| return DemoModeApp::kGoogleSlides; |
| 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.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; |
| |
| return DemoModeApp::kOtherArcApp; |
| } |
| |
| AppType GetAppType(const aura::Window* window) { |
| return static_cast<AppType>(window->GetProperty(aura::client::kAppType)); |
| } |
| |
| bool IsArcWindow(const aura::Window* window) { |
| return (GetAppType(window) == AppType::ARC_APP); |
| } |
| |
| 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)); |
| |
| AppType app_type = GetAppType(window); |
| if (app_type == 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 == extension_misc::kChromeAppId) |
| return DemoModeApp::kBrowser; |
| |
| auto is_default = [](const std::string& app_id) { |
| if (!features::IsMultiProcessMash()) |
| return app_id.empty(); |
| |
| return base::StartsWith(app_id, ShelfWindowWatcher::kDefaultShelfIdPrefix, |
| base::CompareCase::SENSITIVE); |
| }; |
| |
| // If the window is the "browser" type, having an app ID other than the |
| // default indicates a hosted/bookmark app. |
| if (app_type == AppType::CHROME_APP || |
| (app_type == AppType::BROWSER && !is_default(app_id))) { |
| return GetAppFromAppId(app_id); |
| } |
| |
| if (app_type == AppType::BROWSER) |
| return DemoModeApp::kBrowser; |
| return DemoModeApp::kOtherWindow; |
| } |
| |
| } // 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), scoped_observer_(this) {} |
| |
| // 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_observer_.Remove(window); |
| } |
| |
| void OnWindowDestroyed(aura::Window* window) override { |
| if (scoped_observer_.IsObserving(window)) |
| scoped_observer_.Remove(window); |
| } |
| |
| void ObserveWindow(aura::Window* window) { scoped_observer_.Add(window); } |
| |
| private: |
| DemoSessionMetricsRecorder* metrics_recorder_; |
| ScopedObserver<aura::Window, ActiveAppArcPackageNameObserver> |
| scoped_observer_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ActiveAppArcPackageNameObserver); |
| }; |
| |
| // Observes changes in a window's ArcPackageName property for the purpose of |
| // logging of unique launches of ARC apps. |
| class DemoSessionMetricsRecorder::UniqueAppsLaunchedArcPackageNameObserver |
| : public aura::WindowObserver { |
| public: |
| explicit UniqueAppsLaunchedArcPackageNameObserver( |
| DemoSessionMetricsRecorder* metrics_recorder) |
| : metrics_recorder_(metrics_recorder), scoped_observer_(this) {} |
| |
| // 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, AppType::ARC_APP); |
| } else { |
| VLOG(1) << "Got null ARC package name"; |
| } |
| |
| scoped_observer_.Remove(window); |
| } |
| |
| void OnWindowDestroyed(aura::Window* window) override { |
| if (scoped_observer_.IsObserving(window)) |
| scoped_observer_.Remove(window); |
| } |
| |
| void ObserveWindow(aura::Window* window) { scoped_observer_.Add(window); } |
| |
| private: |
| DemoSessionMetricsRecorder* metrics_recorder_; |
| ScopedObserver<aura::Window, UniqueAppsLaunchedArcPackageNameObserver> |
| scoped_observer_; |
| |
| DISALLOW_COPY_AND_ASSIGN(UniqueAppsLaunchedArcPackageNameObserver); |
| }; |
| |
| DemoSessionMetricsRecorder::DemoSessionMetricsRecorder( |
| std::unique_ptr<base::RepeatingTimer> timer) |
| : timer_(std::move(timer)), |
| observer_(this), |
| unique_apps_arc_package_name_observer_( |
| std::make_unique<UniqueAppsLaunchedArcPackageNameObserver>(this)), |
| active_app_arc_package_name_observer_( |
| std::make_unique<ActiveAppArcPackageNameObserver>(this)) { |
| // Outside of tests, use a normal repeating timer. |
| if (!timer_.get()) |
| timer_ = std::make_unique<base::RepeatingTimer>(); |
| |
| StartRecording(); |
| observer_.Add(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() { |
| // Report any remaining stored samples on exit. (If the user went idle, there |
| // won't be any.) |
| ReportSamples(); |
| |
| // Unsubscribe from window activation events. |
| activation_client_->RemoveObserver(this); |
| |
| ReportUniqueAppsLaunched(); |
| } |
| |
| void DemoSessionMetricsRecorder::RecordAppLaunch(const std::string& id, |
| AppType app_type) { |
| if (!ShouldRecordAppLaunch(id)) { |
| return; |
| } |
| DemoModeApp app; |
| if (app_type == 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->type() != aura::client::WINDOW_TYPE_NORMAL) |
| return; |
| |
| AppType app_type = GetAppType(gained_active); |
| |
| std::string app_id; |
| if (app_type == 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) { |
| // 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::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->type() == 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(); |
| } |
| |
| } // namespace ash |