1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/metrics/demo_session_metrics_recorder.h"
6
7 #include <iostream>
8 #include <string>
9 #include <utility>
10
11 #include "ash/public/cpp/app_types.h"
12 #include "ash/public/cpp/window_properties.h"
13 #include "ash/shelf/shelf_window_watcher.h"
14 #include "ash/shell.h"
15 #include "base/metrics/histogram_functions.h"
16 #include "base/metrics/histogram_macros.h"
17 #include "base/scoped_observer.h"
18 #include "base/time/time.h"
19 #include "base/timer/timer.h"
20 #include "extensions/common/constants.h"
21 #include "ui/aura/client/aura_constants.h"
22 #include "ui/aura/client/window_types.h"
23 #include "ui/aura/window.h"
24 #include "ui/base/ui_base_features.h"
25 #include "ui/wm/core/focus_controller.h"
26 #include "ui/wm/public/activation_client.h"
27
28 namespace ash {
29 namespace {
30
31 using DemoModeApp = DemoSessionMetricsRecorder::DemoModeApp;
32
33 // How often to sample.
34 constexpr auto kSamplePeriod = base::TimeDelta::FromSeconds(1);
35
36 // Redefining chromeos::default_web_apps::kHelpAppId as ash can't depend on
37 // chrome.
38 constexpr char kHelpAppId[] = "nbljnnecbjbmifnoehiemkgefbnpoeak";
39
40 // How many periods to wait for user activity before discarding samples.
41 // This timeout is low because demo sessions tend to be very short. If we
42 // recorded samples for a full minute while the device is in between uses, we
43 // would bias our measurements toward whatever app was used last.
44 constexpr int kMaxPeriodsWithoutActivity =
45 base::TimeDelta::FromSeconds(15) / kSamplePeriod;
46
47 // Maps a Chrome app ID to a DemoModeApp value for metrics.
GetAppFromAppId(const std::string & app_id)48 DemoModeApp GetAppFromAppId(const std::string& app_id) {
49 // Each version of the Highlights app is bucketed into the same value.
50 if (app_id == extension_misc::kHighlightsAppId ||
51 app_id == extension_misc::kHighlightsEveAppId ||
52 app_id == extension_misc::kHighlightsNocturneAppId ||
53 app_id == extension_misc::kHighlightsAtlasAppId) {
54 return DemoModeApp::kHighlights;
55 }
56
57 // Each version of the Screensaver app is bucketed into the same value.
58 if (app_id == extension_misc::kScreensaverAppId ||
59 app_id == extension_misc::kScreensaverEveAppId ||
60 app_id == extension_misc::kScreensaverNocturneAppId ||
61 app_id == extension_misc::kScreensaverAtlasAppId ||
62 app_id == extension_misc::kScreensaverKukuiAppId) {
63 return DemoModeApp::kScreensaver;
64 }
65
66 if (app_id == extension_misc::kCameraAppId)
67 return DemoModeApp::kCamera;
68 if (app_id == extension_misc::kChromeAppId)
69 return DemoModeApp::kBrowser;
70 if (app_id == extension_misc::kFilesManagerAppId)
71 return DemoModeApp::kFiles;
72 if (app_id == extension_misc::kCalculatorAppId)
73 return DemoModeApp::kCalculator;
74 if (app_id == extension_misc::kCalendarDemoAppId)
75 return DemoModeApp::kCalendar;
76 if (app_id == extension_misc::kGoogleDocsDemoAppId)
77 return DemoModeApp::kGoogleDocsChromeApp;
78 if (app_id == extension_misc::kGoogleSheetsDemoAppId)
79 return DemoModeApp::kGoogleSheetsChromeApp;
80 if (app_id == extension_misc::kGoogleSlidesDemoAppId)
81 return DemoModeApp::kGoogleSlidesChromeApp;
82 if (app_id == kHelpAppId)
83 return DemoModeApp::kGetHelp;
84 if (app_id == extension_misc::kGoogleKeepAppId)
85 return DemoModeApp::kGoogleKeepChromeApp;
86 if (app_id == extensions::kWebStoreAppId)
87 return DemoModeApp::kWebStore;
88 if (app_id == extension_misc::kYoutubeAppId)
89 return DemoModeApp::kYouTube;
90 return DemoModeApp::kOtherChromeApp;
91 }
92
93 // Maps an ARC++ package name to a DemoModeApp value for metrics.
GetAppFromPackageName(const std::string & package_name)94 DemoModeApp GetAppFromPackageName(const std::string& package_name) {
95 // Google apps.
96 if (package_name == "com.google.Photos" ||
97 package_name == "com.google.android.apps.photos")
98 return DemoModeApp::kGooglePhotos;
99 if (package_name == "com.google.Sheets" ||
100 package_name == "com.google.android.apps.docs.editors.sheets")
101 return DemoModeApp::kGoogleSheetsAndroidApp;
102 if (package_name == "com.google.Slides" ||
103 package_name == "com.google.android.apps.docs.editors.slides")
104 return DemoModeApp::kGoogleSlidesAndroidApp;
105 if (package_name == "com.google.android.keep")
106 return DemoModeApp::kGoogleKeepAndroidApp;
107 if (package_name == "com.android.vending")
108 return DemoModeApp::kPlayStore;
109
110 // Third-party apps.
111 if (package_name == "com.gameloft.android.ANMP.GloftA8HMD")
112 return DemoModeApp::kAsphalt8;
113 if (package_name == "com.gameloft.android.ANMP.GloftA9HM" ||
114 package_name == "com.gameloft.android.ANMP.GloftA9HMD")
115 return DemoModeApp::kAsphalt9;
116 if (package_name == "com.chucklefish.stardewvalley" ||
117 package_name == "com.chucklefish.stardewvalleydemo")
118 return DemoModeApp::kStardewValley;
119 if (package_name == "com.nexstreaming.app.kinemasterfree" ||
120 package_name == "com.nexstreaming.app.kinemasterfree.demo.chromebook")
121 return DemoModeApp::kKinemaster;
122 if (package_name == "com.pixlr.express" ||
123 package_name == "com.pixlr.express.chromebook.demo")
124 return DemoModeApp::kPixlr;
125 if (package_name == "com.brakefield.painter")
126 return DemoModeApp::kInfinitePainter;
127 if (package_name == "com.myscript.nebo.demo")
128 return DemoModeApp::kMyScriptNebo;
129 if (package_name == "com.steadfastinnovation.android.projectpapyrus")
130 return DemoModeApp::kSquid;
131 if (package_name == "com.autodesk.autocadws.demo")
132 return DemoModeApp::kAutoCAD;
133
134 return DemoModeApp::kOtherArcApp;
135 }
136
GetAppType(const aura::Window * window)137 AppType GetAppType(const aura::Window* window) {
138 return static_cast<AppType>(window->GetProperty(aura::client::kAppType));
139 }
140
IsArcWindow(const aura::Window * window)141 bool IsArcWindow(const aura::Window* window) {
142 return (GetAppType(window) == AppType::ARC_APP);
143 }
144
GetArcPackageName(const aura::Window * window)145 const std::string* GetArcPackageName(const aura::Window* window) {
146 DCHECK(IsArcWindow(window));
147 return window->GetProperty(kArcPackageNameKey);
148 }
149
CanGetAppFromWindow(const aura::Window * window)150 bool CanGetAppFromWindow(const aura::Window* window) {
151 // For ARC apps we can only get the App if the package
152 // name is not null.
153 if (IsArcWindow(window)) {
154 return GetArcPackageName(window) != nullptr;
155 }
156 // We can always get the App for non-ARC windows.
157 return true;
158 }
159
GetShelfID(const aura::Window * window)160 const ShelfID GetShelfID(const aura::Window* window) {
161 return ShelfID::Deserialize(window->GetProperty(kShelfIDKey));
162 }
163
164 // Maps the app-like thing in |window| to a DemoModeApp value for metrics.
GetAppFromWindow(const aura::Window * window)165 DemoModeApp GetAppFromWindow(const aura::Window* window) {
166 DCHECK(CanGetAppFromWindow(window));
167
168 AppType app_type = GetAppType(window);
169 if (app_type == AppType::ARC_APP) {
170 // The ShelfID app id isn't used to identify ARC++ apps since it's a hash of
171 // both the package name and the activity.
172 const std::string* package_name = GetArcPackageName(window);
173 return GetAppFromPackageName(*package_name);
174 }
175
176 std::string app_id = GetShelfID(window).app_id;
177
178 // The Chrome "app" in the shelf is just the browser.
179 if (app_id == extension_misc::kChromeAppId)
180 return DemoModeApp::kBrowser;
181
182 // If the window is the "browser" type, having an app ID other than the
183 // default indicates a hosted/bookmark app.
184 if (app_type == AppType::CHROME_APP ||
185 (app_type == AppType::BROWSER && !app_id.empty())) {
186 return GetAppFromAppId(app_id);
187 }
188
189 if (app_type == AppType::BROWSER)
190 return DemoModeApp::kBrowser;
191 return DemoModeApp::kOtherWindow;
192 }
193
194 // Identical to UmaHistogramLongTimes100, but reports times with second
195 // granularity instead of millisecond granularity.
196 // This significantly improves the bucketing if millisecond granularity is
197 // not required - 90/100 buckets are greater than 10 seconds, compared to
198 // 43/100 buckets using millisecond accuracy with min=1ms, or
199 // 72/100 buckets using millisecond accuracy with min=1000ms.
ReportHistogramLongSecondsTimes100(const char * name,base::TimeDelta sample)200 void ReportHistogramLongSecondsTimes100(const char* name,
201 base::TimeDelta sample) {
202 // We use a max of 1 hour = 60 * 60 secs.
203 base::UmaHistogramCustomCounts(name,
204 base::saturated_cast<int>(sample.InSeconds()),
205 /*min=*/1, /*max=*/60 * 60, /*buckets=*/100);
206 }
207
208 } // namespace
209
210 // Observes for changes in a window's ArcPackageName property for the purpose of
211 // logging of active app samples.
212 class DemoSessionMetricsRecorder::ActiveAppArcPackageNameObserver
213 : public aura::WindowObserver {
214 public:
ActiveAppArcPackageNameObserver(DemoSessionMetricsRecorder * metrics_recorder)215 explicit ActiveAppArcPackageNameObserver(
216 DemoSessionMetricsRecorder* metrics_recorder)
217 : metrics_recorder_(metrics_recorder) {}
218
219 // aura::WindowObserver
OnWindowPropertyChanged(aura::Window * window,const void * key,intptr_t old)220 void OnWindowPropertyChanged(aura::Window* window,
221 const void* key,
222 intptr_t old) override {
223 if (key != kArcPackageNameKey)
224 return;
225
226 const std::string* package_name = GetArcPackageName(window);
227
228 if (package_name) {
229 metrics_recorder_->RecordActiveAppSample(
230 GetAppFromPackageName(*package_name));
231 } else {
232 VLOG(1) << "Got null ARC package name";
233 }
234
235 scoped_observer_.Remove(window);
236 }
237
OnWindowDestroyed(aura::Window * window)238 void OnWindowDestroyed(aura::Window* window) override {
239 if (scoped_observer_.IsObserving(window))
240 scoped_observer_.Remove(window);
241 }
242
ObserveWindow(aura::Window * window)243 void ObserveWindow(aura::Window* window) { scoped_observer_.Add(window); }
244
245 private:
246 DemoSessionMetricsRecorder* metrics_recorder_;
247 ScopedObserver<aura::Window, aura::WindowObserver> scoped_observer_{this};
248
249 DISALLOW_COPY_AND_ASSIGN(ActiveAppArcPackageNameObserver);
250 };
251
252 // Observes changes in a window's ArcPackageName property for the purpose of
253 // logging of unique launches of ARC apps.
254 class DemoSessionMetricsRecorder::UniqueAppsLaunchedArcPackageNameObserver
255 : public aura::WindowObserver {
256 public:
UniqueAppsLaunchedArcPackageNameObserver(DemoSessionMetricsRecorder * metrics_recorder)257 explicit UniqueAppsLaunchedArcPackageNameObserver(
258 DemoSessionMetricsRecorder* metrics_recorder)
259 : metrics_recorder_(metrics_recorder) {}
260
261 // aura::WindowObserver
OnWindowPropertyChanged(aura::Window * window,const void * key,intptr_t old)262 void OnWindowPropertyChanged(aura::Window* window,
263 const void* key,
264 intptr_t old) override {
265 if (key != kArcPackageNameKey)
266 return;
267
268 const std::string* package_name = GetArcPackageName(window);
269
270 if (package_name) {
271 metrics_recorder_->RecordAppLaunch(*package_name, AppType::ARC_APP);
272 } else {
273 VLOG(1) << "Got null ARC package name";
274 }
275
276 scoped_observer_.Remove(window);
277 }
278
OnWindowDestroyed(aura::Window * window)279 void OnWindowDestroyed(aura::Window* window) override {
280 if (scoped_observer_.IsObserving(window))
281 scoped_observer_.Remove(window);
282 }
283
ObserveWindow(aura::Window * window)284 void ObserveWindow(aura::Window* window) { scoped_observer_.Add(window); }
285
286 private:
287 DemoSessionMetricsRecorder* metrics_recorder_;
288 ScopedObserver<aura::Window, aura::WindowObserver> scoped_observer_{this};
289
290 DISALLOW_COPY_AND_ASSIGN(UniqueAppsLaunchedArcPackageNameObserver);
291 };
292
DemoSessionMetricsRecorder(std::unique_ptr<base::RepeatingTimer> timer)293 DemoSessionMetricsRecorder::DemoSessionMetricsRecorder(
294 std::unique_ptr<base::RepeatingTimer> timer)
295 : timer_(std::move(timer)),
296 unique_apps_arc_package_name_observer_(
297 std::make_unique<UniqueAppsLaunchedArcPackageNameObserver>(this)),
298 active_app_arc_package_name_observer_(
299 std::make_unique<ActiveAppArcPackageNameObserver>(this)) {
300 // Outside of tests, use a normal repeating timer.
301 if (!timer_.get())
302 timer_ = std::make_unique<base::RepeatingTimer>();
303
304 StartRecording();
305 observer_.Add(ui::UserActivityDetector::Get());
306
307 // Subscribe to window activation updates. Even though this gets us
308 // notifications for all window activations, we ignore the ARC
309 // notifications because they don't contain the app_id. We handle
310 // accounting for ARC windows with OnTaskCreated.
311 if (Shell::Get()->GetPrimaryRootWindow()) {
312 activation_client_ = Shell::Get()->focus_controller();
313 activation_client_->AddObserver(this);
314 }
315 }
316
~DemoSessionMetricsRecorder()317 DemoSessionMetricsRecorder::~DemoSessionMetricsRecorder() {
318 // TODO(mlcui): Investigate whether the metrics emitted here are gracefully
319 // handled during session / device shutdown.
320
321 // Report any remaining stored samples on exit. (If the user went idle, there
322 // won't be any.)
323 ReportSamples();
324
325 ReportDwellTime();
326
327 // Unsubscribe from window activation events.
328 activation_client_->RemoveObserver(this);
329
330 ReportUniqueAppsLaunched();
331 }
332
RecordAppLaunch(const std::string & id,AppType app_type)333 void DemoSessionMetricsRecorder::RecordAppLaunch(const std::string& id,
334 AppType app_type) {
335 if (!ShouldRecordAppLaunch(id)) {
336 return;
337 }
338 DemoModeApp app;
339 if (app_type == AppType::ARC_APP)
340 app = GetAppFromPackageName(id);
341 else
342 app = GetAppFromAppId(id);
343
344 if (!unique_apps_launched_.contains(id)) {
345 unique_apps_launched_.insert(id);
346 // Only log each app launch once. This is determined by
347 // checking the package_name instead of the DemoApp enum,
348 // because the DemoApp enum collapses unknown apps into
349 // a single enum.
350 UMA_HISTOGRAM_ENUMERATION("DemoMode.AppLaunched", app);
351 }
352 }
353
354 // Indicates whether the specified app_id should be recorded for
355 // the unique-apps-launched stat.
ShouldRecordAppLaunch(const std::string & app_id)356 bool DemoSessionMetricsRecorder::ShouldRecordAppLaunch(
357 const std::string& app_id) {
358 return unique_apps_launched_recording_enabled_ &&
359 GetAppFromAppId(app_id) != DemoModeApp::kHighlights &&
360 GetAppFromAppId(app_id) != DemoModeApp::kScreensaver;
361 }
362
OnWindowActivated(ActivationReason reason,aura::Window * gained_active,aura::Window * lost_active)363 void DemoSessionMetricsRecorder::OnWindowActivated(ActivationReason reason,
364 aura::Window* gained_active,
365 aura::Window* lost_active) {
366 if (!gained_active)
367 return;
368
369 // Don't count popup windows.
370 if (gained_active->type() != aura::client::WINDOW_TYPE_NORMAL)
371 return;
372
373 AppType app_type = GetAppType(gained_active);
374
375 std::string app_id;
376 if (app_type == AppType::ARC_APP) {
377 const std::string* package_name = GetArcPackageName(gained_active);
378
379 if (!package_name) {
380 // The package name property for the window has not been set yet.
381 // Listen for changes to the window properties so we can
382 // be informed when the package name gets set.
383 if (!gained_active->HasObserver(
384 unique_apps_arc_package_name_observer_.get())) {
385 unique_apps_arc_package_name_observer_->ObserveWindow(gained_active);
386 }
387 return;
388 }
389 app_id = *package_name;
390 } else {
391 // This is a non-ARC window, so we just get the shelf ID, which should
392 // be unique per app.
393 app_id = GetShelfID(gained_active).app_id;
394 }
395
396 // Some app_ids are empty, i.e the "You will be signed out
397 // in X seconds" modal dialog in Demo Mode, so skip those.
398 if (app_id.empty())
399 return;
400
401 RecordAppLaunch(app_id, app_type);
402 }
403
OnUserActivity(const ui::Event * event)404 void DemoSessionMetricsRecorder::OnUserActivity(const ui::Event* event) {
405 // Record the first and last time activity was observed.
406 if (first_user_activity_.is_null()) {
407 first_user_activity_ = base::TimeTicks::Now();
408 }
409 last_user_activity_ = base::TimeTicks::Now();
410
411 // Report samples recorded since the last activity.
412 ReportSamples();
413
414 // Restart the timer if the device has been idle.
415 if (!timer_->IsRunning())
416 StartRecording();
417 periods_since_activity_ = 0;
418 }
419
StartRecording()420 void DemoSessionMetricsRecorder::StartRecording() {
421 unique_apps_launched_recording_enabled_ = true;
422 timer_->Start(FROM_HERE, kSamplePeriod, this,
423 &DemoSessionMetricsRecorder::TakeSampleOrPause);
424 }
425
RecordActiveAppSample(DemoModeApp app)426 void DemoSessionMetricsRecorder::RecordActiveAppSample(DemoModeApp app) {
427 unreported_samples_.push_back(app);
428 }
429
TakeSampleOrPause()430 void DemoSessionMetricsRecorder::TakeSampleOrPause() {
431 // After enough inactive time, assume the user left.
432 if (++periods_since_activity_ > kMaxPeriodsWithoutActivity) {
433 // These samples were collected since the last user activity.
434 unreported_samples_.clear();
435 timer_->Stop();
436 return;
437 }
438
439 aura::Window* window = Shell::Get()->activation_client()->GetActiveWindow();
440 if (!window)
441 return;
442
443 // If there is no ARC package name available, set up a listener
444 // to be informed when it is available.
445 if (IsArcWindow(window) && !CanGetAppFromWindow(window)) {
446 active_app_arc_package_name_observer_->ObserveWindow(window);
447 return;
448 }
449
450 DemoModeApp app = window->type() == aura::client::WINDOW_TYPE_NORMAL
451 ? GetAppFromWindow(window)
452 : DemoModeApp::kOtherWindow;
453 RecordActiveAppSample(app);
454 }
455
ReportSamples()456 void DemoSessionMetricsRecorder::ReportSamples() {
457 for (DemoModeApp app : unreported_samples_)
458 UMA_HISTOGRAM_ENUMERATION("DemoMode.ActiveApp", app);
459 unreported_samples_.clear();
460 }
461
ReportUniqueAppsLaunched()462 void DemoSessionMetricsRecorder::ReportUniqueAppsLaunched() {
463 if (unique_apps_launched_recording_enabled_)
464 UMA_HISTOGRAM_COUNTS_100("DemoMode.UniqueAppsLaunched",
465 unique_apps_launched_.size());
466 unique_apps_launched_.clear();
467 }
468
ReportDwellTime()469 void DemoSessionMetricsRecorder::ReportDwellTime() {
470 if (!first_user_activity_.is_null()) {
471 DCHECK(!last_user_activity_.is_null());
472 DCHECK_LE(first_user_activity_, last_user_activity_);
473
474 base::TimeDelta dwell_time = last_user_activity_ - first_user_activity_;
475 ReportHistogramLongSecondsTimes100("DemoMode.DwellTime", dwell_time);
476 }
477 first_user_activity_ = base::TimeTicks();
478 last_user_activity_ = base::TimeTicks();
479 }
480
481 } // namespace ash
482