chromium/chrome/browser/apps/app_service/metrics/app_platform_input_metrics.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/apps/app_service/metrics/app_platform_input_metrics.h"

#include "ash/shell.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics_utils.h"
#include "chrome/browser/apps/app_service/web_contents_app_id_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chromeos/components/mgs/managed_guest_session_utils.h"
#include "components/app_constants/constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/instance_update.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "ui/aura/window.h"
#include "ui/events/event_constants.h"
#include "ui/events/types/event_type.h"
#include "ui/views/widget/widget.h"

namespace apps {

namespace {

constexpr char kInputEventMouseKey[] = "mouse";
constexpr char kInputEventStylusKey[] = "stylus";
constexpr char kInputEventTouchKey[] = "touch";
constexpr char kInputEventKeyboardKey[] = "keyboard";

base::flat_map<std::string, InputEventSource>& GetInputEventSourceMap() {
  static base::NoDestructor<base::flat_map<std::string, InputEventSource>>
      input_event_source_map;
  if (input_event_source_map->empty()) {
    *input_event_source_map = {
        {kInputEventMouseKey, InputEventSource::kMouse},
        {kInputEventStylusKey, InputEventSource::kStylus},
        {kInputEventTouchKey, InputEventSource::kTouch},
        {kInputEventKeyboardKey, InputEventSource::kKeyboard},
    };
  }
  return *input_event_source_map;
}

InputEventSource GetInputEventSource(ui::EventPointerType type) {
  switch (type) {
    case ui::EventPointerType::kUnknown:
      return InputEventSource::kUnknown;
    case ui::EventPointerType::kMouse:
      return InputEventSource::kMouse;
    case ui::EventPointerType::kPen:
      return InputEventSource::kStylus;
    case ui::EventPointerType::kTouch:
      return InputEventSource::kTouch;
    case ui::EventPointerType::kEraser:
      return InputEventSource::kStylus;
  }
}

// Returns the input event source for the given `event_source` string.
InputEventSource GetInputEventSourceFromString(
    const std::string& event_source) {
  const auto& input_event_source_map = GetInputEventSourceMap();
  auto it = input_event_source_map.find(event_source);
  return (it != input_event_source_map.end()) ? it->second
                                              : InputEventSource::kUnknown;
}

// Returns the string key for `event_source` to save input events in the user
// pref.
std::string GetInputEventSourceKey(InputEventSource event_source) {
  switch (event_source) {
    case InputEventSource::kUnknown:
      return std::string();
    case InputEventSource::kMouse:
      return kInputEventMouseKey;
    case InputEventSource::kStylus:
      return kInputEventStylusKey;
    case InputEventSource::kTouch:
      return kInputEventTouchKey;
    case InputEventSource::kKeyboard:
      return kInputEventKeyboardKey;
  }
}

base::Value::Dict ConvertEventCountsToValue(
    const AppPlatformInputMetrics::EventSourceToCounts& event_counts) {
  base::Value::Dict event_counts_dict;
  for (const auto& counts : event_counts) {
    base::Value::Dict count_dict;
    for (const auto& it : counts.second) {
      count_dict.Set(GetAppTypeHistogramName(it.first), it.second);
    }
    event_counts_dict.Set(GetInputEventSourceKey(counts.first),
                          std::move(count_dict));
  }
  return event_counts_dict;
}

AppPlatformInputMetrics::EventSourceToCounts ConvertDictValueToEventCounts(
    const base::Value::Dict& event_counts) {
  AppPlatformInputMetrics::EventSourceToCounts ret;
  for (const auto [app_id, counts] : event_counts) {
    auto event_source = GetInputEventSourceFromString(app_id);
    if (event_source == InputEventSource::kUnknown) {
      continue;
    }

    const base::Value::Dict* counts_dict = counts.GetIfDict();
    if (!counts_dict) {
      continue;
    }
    for (const auto [app_type, count_value] : *counts_dict) {
      auto app_type_name = GetAppTypeNameFromString(app_type);
      if (app_type_name == AppTypeName::kUnknown) {
        continue;
      }

      auto count = count_value.GetIfInt();
      if (!count.has_value()) {
        continue;
      }
      ret[event_source][app_type_name] = count.value();
    }
  }
  return ret;
}

}  // namespace

constexpr char kAppInputEventsKey[] = "app_platform_metrics.app_input_events";

AppPlatformInputMetrics::AppPlatformInputMetrics(
    Profile* profile,
    const apps::AppRegistryCache& app_registry_cache,
    InstanceRegistry& instance_registry)
    : profile_(profile), app_registry_cache_(app_registry_cache) {
  instance_registry_observation_.Observe(&instance_registry);
  if (chromeos::IsManagedGuestSession()) {
    CHECK(ukm::UkmRecorder::Get());
    ukm_recorder_observer_.Observe(ukm::UkmRecorder::Get());
  }
  if (ash::Shell::HasInstance()) {
    ash::Shell::Get()->AddPreTargetHandler(this);
  }
}

AppPlatformInputMetrics::~AppPlatformInputMetrics() {
  if (ash::Shell::HasInstance()) {
    ash::Shell::Get()->RemovePreTargetHandler(this);
  }
}

void AppPlatformInputMetrics::OnMouseEvent(ui::MouseEvent* event) {
  if (event->type() == ui::EventType::kMouseReleased) {
    RecordEventCount(GetInputEventSource(event->pointer_details().pointer_type),
                     event->target());
  }
}

void AppPlatformInputMetrics::OnKeyEvent(ui::KeyEvent* event) {
  if (event->type() == ui::EventType::kKeyReleased) {
    RecordEventCount(InputEventSource::kKeyboard, event->target());
  }
}

void AppPlatformInputMetrics::OnTouchEvent(ui::TouchEvent* event) {
  if (event->type() == ui::EventType::kTouchReleased) {
    RecordEventCount(GetInputEventSource(event->pointer_details().pointer_type),
                     event->target());
  }
}

void AppPlatformInputMetrics::OnFiveMinutes() {
  // For the first five minutes, since the saved input events in pref haven't
  // been recorded yet, read the input events saved in the user pref, and record
  // the input events UKM, then save the new input events to the user pref.
  if (should_record_ukm_from_pref_) {
    RecordInputEventsAppKMFromPref();
    should_record_ukm_from_pref_ = false;
  }
  SaveInputEvents();
}

void AppPlatformInputMetrics::OnTwoHours() {
  RecordInputEventsAppKM();
}

void AppPlatformInputMetrics::OnInstanceUpdate(const InstanceUpdate& update) {
  if (!update.StateChanged()) {
    return;
  }

  aura::Window* window = update.Window();
  if (update.State() & InstanceState::kDestroyed) {
    window_to_app_info_.erase(window);
    browser_to_tab_list_.RemoveActivatedTab(update.InstanceId());
    return;
  }

  auto app_id = update.AppId();
  auto app_type = GetAppType(profile_, app_id);

  // For apps, not opened with browser windows, the app id and app type should
  // not change. So if we have the app info for the window, we don't need to
  // update it.
  if (base::Contains(window_to_app_info_, window) &&
      !IsAppOpenedWithBrowserWindow(profile_, app_type, app_id)) {
    return;
  }

  if (update.State() & apps::InstanceState::kActive) {
    SetAppInfoForActivatedWindow(app_type, app_id, window, update.InstanceId());
  } else {
    SetAppInfoForInactivatedWindow(update.InstanceId());
  }
}

void AppPlatformInputMetrics::OnInstanceRegistryWillBeDestroyed(
    InstanceRegistry* cache) {
  instance_registry_observation_.Reset();
}

void AppPlatformInputMetrics::OnStartingShutdown() {
  CHECK(chromeos::IsManagedGuestSession());
  RecordInputEventsAppKM();
}

void AppPlatformInputMetrics::SetAppInfoForActivatedWindow(
    AppType app_type,
    const std::string& app_id,
    aura::Window* window,
    const base::UnguessableToken& instance_id) {
  // For the browser window, if a tab of the browser is activated, we don't
  // need to update, because we can reuse the active tab's app id.
  if (app_id == app_constants::kChromeAppId &&
      browser_to_tab_list_.HasActivatedTab(window)) {
    return;
  }

  AppTypeName app_type_name =
      GetAppTypeNameForWindow(profile_, app_type, app_id, window);
  if (app_type_name == AppTypeName::kUnknown) {
    return;
  }

  // For apps opened in browser windows, get the top level window, and modify
  // `browser_to_tab_list_` to save the activated tab app id.
  if (IsAppOpenedWithBrowserWindow(profile_, app_type, app_id)) {
    window = IsLacrosWindow(window) ? window : window->GetToplevelWindow();
    if (IsAppOpenedInTab(app_type_name, app_id)) {
      // When the tab is pulled to a separate browser window, the instance id is
      // not changed, but the parent browser window is changed. So remove the
      // tab window instance from previous browser window, and add it to the new
      // browser window.
      browser_to_tab_list_.RemoveActivatedTab(instance_id);
      browser_to_tab_list_.AddActivatedTab(window, instance_id, app_id);
    }
  }

  window_to_app_info_[window].app_id = app_id;
  window_to_app_info_[window].app_type_name = app_type_name;
}

void AppPlatformInputMetrics::SetAppInfoForInactivatedWindow(
    const base::UnguessableToken& instance_id) {
  // When the window is inactived, only modify the app info for browser windows,
  // because the activated tab changing might affect the app id for browser
  // windows.
  //
  // For apps, not opened with browser windows,  the app id and app type should
  // not be changed for non-browser windows, and they can be modified when the
  // window is activated.
  auto* browser_window = browser_to_tab_list_.GetBrowserWindow(instance_id);
  if (!browser_window) {
    return;
  }

  browser_to_tab_list_.RemoveActivatedTab(instance_id);

  auto app_id = browser_to_tab_list_.GetActivatedTabAppId(browser_window);
  if (app_id.empty()) {
    app_id = IsLacrosWindow(browser_window) ? app_constants::kLacrosAppId
                                            : app_constants::kChromeAppId;
  }

  window_to_app_info_[browser_window].app_id = app_id;
  window_to_app_info_[browser_window].app_type_name =
      IsLacrosWindow(browser_window) ? apps::AppTypeName::kStandaloneBrowser
                                     : apps::AppTypeName::kChromeBrowser;
}

void AppPlatformInputMetrics::RecordEventCount(InputEventSource event_source,
                                               ui::EventTarget* event_target) {
  views::Widget* target = views::Widget::GetTopLevelWidgetForNativeView(
      static_cast<aura::Window*>(event_target));
  if (!target) {
    return;
  }

  aura::Window* top_window = target->GetNativeWindow();
  if (!top_window) {
    return;
  }

  auto it = window_to_app_info_.find(top_window);
  if (it == window_to_app_info_.end()) {
    return;
  }

  if (!ShouldRecordAppKMForApp(it->second.app_id)) {
    return;
  }

  ++app_id_to_event_count_per_two_hours_[it->second.app_id][event_source]
                                        [it->second.app_type_name];
}

void AppPlatformInputMetrics::RecordInputEventsAppKM() {
  if (!ShouldRecordAppKM(profile_)) {
    return;
  }

  for (const auto& event_counts : app_id_to_event_count_per_two_hours_) {
    if (!ShouldRecordAppKMForApp(event_counts.first)) {
      continue;
    }
    // `event_counts.second` is the map from InputEventSource to the event
    // counts.
    RecordInputEventsAppKMForApp(event_counts.first, event_counts.second);
  }

  app_id_to_event_count_per_two_hours_.clear();
}

void AppPlatformInputMetrics::RecordInputEventsAppKMForApp(
    const std::string& app_id,
    const EventSourceToCounts& event_counts) {
  for (const auto& counts : event_counts) {
    InputEventSource event_source = counts.first;

    // `counts.second` is the map from AppTypeName to the event count.
    for (const auto& count : counts.second) {
      auto source_id = AppPlatformMetrics::GetSourceId(profile_, app_id);
      if (source_id == ukm::kInvalidSourceId) {
        continue;
      }
      ukm::builders::ChromeOSApp_InputEvent builder(source_id);
      builder.SetAppType((int)count.first)
          .SetAppInputEventSource((int)event_source)
          .SetAppInputEventCount(count.second)
          .SetUserDeviceMatrix(GetUserTypeByDeviceTypeMetrics())
          .Record(ukm::UkmRecorder::Get());
      AppPlatformMetrics::RemoveSourceId(source_id);
    }
  }
}

void AppPlatformInputMetrics::SaveInputEvents() {
  ScopedDictPrefUpdate input_events_update(profile_->GetPrefs(),
                                           kAppInputEventsKey);
  input_events_update->clear();
  for (const auto& event_counts : app_id_to_event_count_per_two_hours_) {
    input_events_update->SetByDottedPath(
        event_counts.first, ConvertEventCountsToValue(event_counts.second));
  }
}

void AppPlatformInputMetrics::RecordInputEventsAppKMFromPref() {
  if (!ShouldRecordAppKM(profile_)) {
    return;
  }

  ScopedDictPrefUpdate input_events_update(profile_->GetPrefs(),
                                           kAppInputEventsKey);

  for (const auto [app_id, events] : *input_events_update) {
    if (!ShouldRecordAppKMForApp(app_id)) {
      continue;
    }

    const base::Value::Dict* events_dict = events.GetIfDict();
    if (!events_dict) {
      continue;
    }

    EventSourceToCounts event_counts =
        ConvertDictValueToEventCounts(*events_dict);
    RecordInputEventsAppKMForApp(app_id, event_counts);
  }
}

bool AppPlatformInputMetrics::ShouldRecordAppKMForApp(
    const std::string& app_id) {
  return ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(),
                                   app_id) &&
         ShouldRecordAppKMForAppTypeName(GetAppType(profile_, app_id));
}

}  // namespace apps