blob: 7bd4c84e7f2882ac780e2a976a3d7efec6a81af3 [file] [log] [blame]
// 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 <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/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/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;
// Returns a package name for an ARC window. The returned value is unowned and
// may be null.
const std::string* GetPackageNameForArcWindow(const aura::Window* window) {
// Make sure this is an ARC app window.
DCHECK(static_cast<ash::AppType>(window->GetProperty(
aura::client::kAppType)) == ash::AppType::ARC_APP);
// We use a dedicated key for instead of kShelfIDKey for identifiying ARC++
// apps. The ShelfID app id isn't used to identify ARC++ apps since it's a
// hash of both the package name and the activity.
return static_cast<std::string*>(window->GetProperty(kArcPackageNameKey));
}
// Returns the app ID For a non-ARC window. This function crashes if the
// window is an ARC window.
std::string GetAppIdForWindow(const aura::Window* window) {
DCHECK(static_cast<ash::AppType>(window->GetProperty(
aura::client::kAppType)) != ash::AppType::ARC_APP);
return ShelfID::Deserialize(window->GetProperty(kShelfIDKey)).app_id;
}
// 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;
}
if (app_id == extension_misc::kCameraAppId)
return DemoModeApp::kCamera;
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;
}
// Maps the app-like thing in |window| to a DemoModeApp value for metrics.
DemoModeApp GetAppFromWindow(const aura::Window* window) {
ash::AppType app_type =
static_cast<ash::AppType>(window->GetProperty(aura::client::kAppType));
if (app_type == ash::AppType::ARC_APP) {
const std::string* package_name = GetPackageNameForArcWindow(window);
return package_name ? GetAppFromPackageName(*package_name)
: DemoModeApp::kOtherArcApp;
}
std::string app_id = GetAppIdForWindow(window);
// 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::IsUsingWindowService())
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 == ash::AppType::CHROME_APP ||
(app_type == ash::AppType::BROWSER && !is_default(app_id))) {
return GetAppFromAppId(app_id);
}
if (app_type == ash::AppType::BROWSER)
return DemoModeApp::kBrowser;
return DemoModeApp::kOtherWindow;
}
} // namespace
DemoSessionMetricsRecorder::DemoSessionMetricsRecorder(
std::unique_ptr<base::RepeatingTimer> timer)
: timer_(std::move(timer)), observer_(this) {
// Outside of tests, use a normal repeating timer.
if (!timer_.get())
timer_ = std::make_unique<base::RepeatingTimer>();
StartRecording();
// Listen for user activity events.
observer_.Add(ui::UserActivityDetector::Get());
// Listen for Window activation events.
Shell::Get()->activation_client()->AddObserver(this);
}
DemoSessionMetricsRecorder::~DemoSessionMetricsRecorder() {
// Report any remaining stored samples on exit. (If the user went idle, there
// won't be any.)
ReportSamples();
// Stop listening for window activation events.
Shell::Get()->activation_client()->RemoveObserver(this);
// Report the number of unique apps launched since the last
// time we reported, and when the demo session ends. Only
// do this if there are any entries in the set, because an idle
// session that was shut down can result in erroneous
// sample stating that 0 unique apps were launched.
if (unique_apps_launched_.size() > 0)
ReportUniqueAppsLaunched();
}
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::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
if (gained_active == nullptr)
return;
// Don't count popup windows.
if (gained_active->type() != aura::client::WINDOW_TYPE_NORMAL)
return;
// Track unique apps opened. There is no namespace collision between
// ARC apps and ChromeOS Apps because ARC app package names use a different
// naming scheme than ChromeOS Apps.
std::string unique_app_id;
ash::AppType app_type = static_cast<ash::AppType>(
gained_active->GetProperty(aura::client::kAppType));
if (app_type == ash::AppType::ARC_APP) {
const std::string* package_name = GetPackageNameForArcWindow(gained_active);
unique_app_id = *package_name;
} else {
unique_app_id = GetAppIdForWindow(gained_active);
}
unique_apps_launched_.insert(unique_app_id);
}
void DemoSessionMetricsRecorder::StartRecording() {
timer_->Start(FROM_HERE, kSamplePeriod, this,
&DemoSessionMetricsRecorder::TakeSampleOrPause);
}
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();
// Since we are assuming that the user left, report how many
// unique apps have been launched since we last reported.
ReportUniqueAppsLaunched();
return;
}
const aura::Window* window =
ash::Shell::Get()->activation_client()->GetActiveWindow();
if (!window)
return;
DemoModeApp app = window->type() == aura::client::WINDOW_TYPE_NORMAL
? GetAppFromWindow(window)
: DemoModeApp::kOtherWindow;
unreported_samples_.push_back(app);
}
void DemoSessionMetricsRecorder::ReportSamples() {
for (DemoModeApp app : unreported_samples_)
UMA_HISTOGRAM_ENUMERATION("DemoMode.ActiveApp", app);
unreported_samples_.clear();
}
void DemoSessionMetricsRecorder::ReportUniqueAppsLaunched() {
UMA_HISTOGRAM_COUNTS_100("DemoMode.UniqueAppsLaunched",
unique_apps_launched_.size());
unique_apps_launched_.clear();
}
} // namespace ash