blob: d337bff8d63fd578c4c682913c0ff965a17d06aa [file] [log] [blame]
// 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/app_types.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/scoped_multi_source_observation.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/app_constants/constants.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/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::Seconds(1);
// Redefining chromeos::preinstalled_web_apps::kHelpAppId as ash can't depend on
// chrome.
constexpr char kHelpAppId[] = "nbljnnecbjbmifnoehiemkgefbnpoeak";
// 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) {
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)
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::kGoogleDocsDemoAppId)
return DemoModeApp::kGoogleDocsChromeApp;
if (app_id == extension_misc::kGoogleDocsPwaAppId)
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;
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" ||
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;
return DemoModeApp::kOtherArcApp;
}
AppType GetAppType(const aura::Window* window) {
return static_cast<AppType>(window->GetProperty(aura::client::kAppType));
}
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 == 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 == AppType::CHROME_APP ||
(app_type == AppType::BROWSER && !app_id.empty())) {
return GetAppFromAppId(app_id);
}
if (app_type == 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);
}
} // 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) {
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.
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, 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};
};
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)) {
// 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(mlcui): Investigate whether the metrics emitted here are gracefully
// handled during session / device shutdown.
// Report any remaining stored samples on exit. (If the user went idle, there
// won't be any.)
ReportSamples();
ReportDwellTime();
ReportUserClickesAndPresses();
// 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->GetType() != 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) {
// Record the first and last time activity was observed.
if (first_user_activity_.is_null()) {
first_user_activity_ = base::TimeTicks::Now();
}
last_user_activity_ = base::TimeTicks::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::ET_MOUSE_PRESSED) {
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::ET_TOUCH_PRESSED) {
user_clicks_and_presses_++;
}
}
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