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