chromium/chrome/browser/apps/app_service/metrics/app_platform_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_metrics.h"

#include <memory>
#include <set>
#include <string_view>

#include "base/check_deref.h"
#include "base/containers/fixed_flat_set.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics_utils.h"
#include "chrome/browser/apps/app_service/metrics/app_service_metrics.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/guest_os_shelf_utils.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.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/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/instance.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/ukm/app_source_url_recorder.h"
#include "components/user_manager/user_manager.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "ui/aura/window.h"

namespace {

// UMA metrics for a snapshot count of installed apps.
constexpr char kAppsCountHistogramPrefix[] = "Apps.AppsCount.";
constexpr char kAppsCountPerInstallReasonHistogramPrefix[] =
    "Apps.AppsCountPerInstallReason.";
constexpr char kAppsRunningDurationHistogramPrefix[] = "Apps.RunningDuration.";
constexpr char kAppsRunningPercentageHistogramPrefix[] =
    "Apps.RunningPercentage.";
constexpr char kAppsActivatedCountHistogramPrefix[] = "Apps.ActivatedCount.";
constexpr char kAppsUsageTimeHistogramPrefix[] = "Apps.UsageTime.";
constexpr char kAppsUsageTimeHistogramPrefixV2[] = "Apps.UsageTimeV2.";

constexpr base::TimeDelta kMaxDuration = base::Days(1);

constexpr auto kAppTypeNameSet = base::MakeFixedFlatSet<apps::AppTypeName>({
    apps::AppTypeName::kArc,
    apps::AppTypeName::kBuiltIn,
    apps::AppTypeName::kCrostini,
    apps::AppTypeName::kChromeApp,
    apps::AppTypeName::kWeb,
    apps::AppTypeName::kPluginVm,
    apps::AppTypeName::kStandaloneBrowser,
    apps::AppTypeName::kRemote,
    apps::AppTypeName::kBorealis,
    apps::AppTypeName::kSystemWeb,
    apps::AppTypeName::kChromeBrowser,
    apps::AppTypeName::kStandaloneBrowserChromeApp,
    apps::AppTypeName::kExtension,
    apps::AppTypeName::kStandaloneBrowserExtension,
    apps::AppTypeName::kStandaloneBrowserWebApp,
    apps::AppTypeName::kBruschetta,
});

// Returns AppTypeNameV2 used for app running metrics.
apps::AppTypeNameV2 GetAppTypeNameV2(Profile* profile,
                                     apps::AppType app_type,
                                     const std::string& app_id,
                                     aura::Window* window) {
  switch (app_type) {
    case apps::AppType::kUnknown:
      return apps::AppTypeNameV2::kUnknown;
    case apps::AppType::kArc:
      return apps::AppTypeNameV2::kArc;
    case apps::AppType::kBuiltIn:
      return apps::AppTypeNameV2::kBuiltIn;
    case apps::AppType::kCrostini:
      return apps::AppTypeNameV2::kCrostini;
    case apps::AppType::kChromeApp:
      return app_id == app_constants::kChromeAppId
                 ? apps::AppTypeNameV2::kChromeBrowser
             : apps::IsAshBrowserWindow(window)
                 ? apps::AppTypeNameV2::kChromeAppTab
                 : apps::AppTypeNameV2::kChromeAppWindow;
    case apps::AppType::kWeb: {
      apps::AppTypeName app_type_name =
          apps::GetAppTypeNameForWebAppWindow(profile, app_id, window);
      if (app_type_name == apps::AppTypeName::kChromeBrowser) {
        return apps::AppTypeNameV2::kWebTab;
      } else if (app_type_name == apps::AppTypeName::kStandaloneBrowser) {
        return apps::AppTypeNameV2::kStandaloneBrowserWebAppTab;
      } else if (app_type_name == apps::AppTypeName::kSystemWeb) {
        return apps::AppTypeNameV2::kSystemWeb;
      } else if (crosapi::browser_util::IsLacrosEnabled()) {
        return apps::AppTypeNameV2::kStandaloneBrowserWebAppWindow;
      } else {
        return apps::AppTypeNameV2::kWebWindow;
      }
    }
    case apps::AppType::kPluginVm:
      return apps::AppTypeNameV2::kPluginVm;
    case apps::AppType::kStandaloneBrowser:
      return apps::AppTypeNameV2::kStandaloneBrowser;
    case apps::AppType::kRemote:
      return apps::AppTypeNameV2::kRemote;
    case apps::AppType::kBorealis:
      return apps::AppTypeNameV2::kBorealis;
    case apps::AppType::kSystemWeb:
      return apps::AppTypeNameV2::kSystemWeb;
    case apps::AppType::kStandaloneBrowserChromeApp:
      return apps::IsLacrosBrowserWindow(profile, window)
                 ? apps::AppTypeNameV2::kStandaloneBrowserChromeAppTab
                 : apps::AppTypeNameV2::kStandaloneBrowserChromeAppWindow;
    case apps::AppType::kExtension:
      return apps::AppTypeNameV2::kExtension;
    case apps::AppType::kStandaloneBrowserExtension:
      return apps::AppTypeNameV2::kStandaloneBrowserExtension;
    case apps::AppType::kBruschetta:
      return apps::AppTypeNameV2::kBruschetta;
  }
}

// Returns AppTypeNameV2 used for app launch metrics.
apps::AppTypeNameV2 GetAppTypeNameV2(Profile* profile,
                                     apps::AppType app_type,
                                     const std::string& app_id,
                                     apps::LaunchContainer container) {
  switch (app_type) {
    case apps::AppType::kUnknown:
      return apps::AppTypeNameV2::kUnknown;
    case apps::AppType::kArc:
      return apps::AppTypeNameV2::kArc;
    case apps::AppType::kBuiltIn:
      return apps::AppTypeNameV2::kBuiltIn;
    case apps::AppType::kCrostini:
      return apps::AppTypeNameV2::kCrostini;
    case apps::AppType::kChromeApp:
      return container == apps::LaunchContainer::kLaunchContainerWindow
                 ? apps::AppTypeNameV2::kChromeAppWindow
                 : apps::AppTypeNameV2::kChromeAppTab;
    case apps::AppType::kWeb: {
      apps::AppTypeName app_type_name =
          apps::GetAppTypeNameForWebApp(profile, app_id, container);
      if (app_type_name == apps::AppTypeName::kChromeBrowser) {
        return apps::AppTypeNameV2::kWebTab;
      } else if (app_type_name == apps::AppTypeName::kStandaloneBrowser) {
        return apps::AppTypeNameV2::kStandaloneBrowserWebAppTab;
      } else if (app_type_name == apps::AppTypeName::kSystemWeb) {
        return apps::AppTypeNameV2::kSystemWeb;
      } else if (crosapi::browser_util::IsLacrosEnabled()) {
        return apps::AppTypeNameV2::kStandaloneBrowserWebAppWindow;
      } else {
        return apps::AppTypeNameV2::kWebWindow;
      }
    }
    case apps::AppType::kPluginVm:
      return apps::AppTypeNameV2::kPluginVm;
    case apps::AppType::kStandaloneBrowser:
      return apps::AppTypeNameV2::kStandaloneBrowser;
    case apps::AppType::kRemote:
      return apps::AppTypeNameV2::kRemote;
    case apps::AppType::kBorealis:
      return apps::AppTypeNameV2::kBorealis;
    case apps::AppType::kSystemWeb:
      return apps::AppTypeNameV2::kSystemWeb;
    case apps::AppType::kBruschetta:
      return apps::AppTypeNameV2::kBruschetta;
    case apps::AppType::kStandaloneBrowserChromeApp: {
      apps::AppTypeName app_type_name =
          apps::GetAppTypeNameForStandaloneBrowserChromeApp(profile, app_id,
                                                            container);
      return app_type_name == apps::AppTypeName::kStandaloneBrowser
                 ? apps::AppTypeNameV2::kStandaloneBrowserChromeAppTab
                 : apps::AppTypeNameV2::kStandaloneBrowserChromeAppWindow;
    }
    case apps::AppType::kExtension:
      return apps::AppTypeNameV2::kExtension;
    case apps::AppType::kStandaloneBrowserExtension:
      return apps::AppTypeNameV2::kStandaloneBrowserExtension;
  }
}

// Records the number of times Chrome OS apps are launched grouped by the launch
// source.
void RecordAppLaunchSource(apps::LaunchSource launch_source) {
  base::UmaHistogramEnumeration("Apps.AppLaunchSource", launch_source);
}

// Records the number of times Chrome OS apps are launched grouped by the app
// type.
void RecordAppLaunchPerAppType(apps::AppTypeName app_type_name) {
  if (app_type_name == apps::AppTypeName::kUnknown) {
    return;
  }

  base::UmaHistogramEnumeration(apps::kAppLaunchPerAppTypeHistogramName,
                                app_type_name);
}

// Records the number of times Chrome OS apps are launched grouped by the app
// type V2.
void RecordAppLaunchPerAppTypeV2(apps::AppTypeNameV2 app_type_name_v2) {
  if (app_type_name_v2 == apps::AppTypeNameV2::kUnknown) {
    return;
  }

  base::UmaHistogramEnumeration(apps::kAppLaunchPerAppTypeV2HistogramName,
                                app_type_name_v2);
}

base::TimeDelta GetDurationAndResetStartTime(base::TimeTicks& start_time) {
  base::TimeDelta duration = base::TimeTicks::Now() - start_time;
  start_time = base::TimeTicks::Now();
  return duration;
}

}  // namespace

namespace apps {

constexpr char kAppRunningDuration[] =
    "app_platform_metrics.app_running_duration";
constexpr char kAppActivatedCount[] =
    "app_platform_metrics.app_activated_count";
constexpr char kAppUsageTime[] = "app_platform_metrics.app_usage_time";

constexpr char kAppLaunchPerAppTypeHistogramName[] = "Apps.AppLaunchPerAppType";
constexpr char kAppLaunchPerAppTypeV2HistogramName[] =
    "Apps.AppLaunchPerAppTypeV2";

constexpr char kChromeAppTabHistogramName[] = "ChromeAppTab";
constexpr char kChromeAppWindowHistogramName[] = "ChromeAppWindow";
constexpr char kWebAppTabHistogramName[] = "WebAppTab";
constexpr char kWebAppWindowHistogramName[] = "WebAppWindow";

constexpr char kUsageTimeAppIdKey[] = "app_id";
constexpr char kUsageTimeAppPublisherIdKey[] = "app_publisher_id";
constexpr char kUsageTimeAppTypeKey[] = "app_type";
constexpr char kUsageTimeDurationKey[] = "time";
constexpr char kReportingUsageTimeDurationKey[] = "reporting_usage_time";

std::string GetAppTypeHistogramNameV2(apps::AppTypeNameV2 app_type_name) {
  switch (app_type_name) {
    case apps::AppTypeNameV2::kUnknown:
      return std::string();
    case apps::AppTypeNameV2::kArc:
      return kArcHistogramName;
    case apps::AppTypeNameV2::kBuiltIn:
      return kBuiltInHistogramName;
    case apps::AppTypeNameV2::kCrostini:
      return kCrostiniHistogramName;
    case apps::AppTypeNameV2::kChromeAppWindow:
      return kChromeAppWindowHistogramName;
    case apps::AppTypeNameV2::kChromeAppTab:
      return kChromeAppTabHistogramName;
    case apps::AppTypeNameV2::kWebWindow:
      return kWebAppWindowHistogramName;
    case apps::AppTypeNameV2::kWebTab:
      return kWebAppTabHistogramName;
    case apps::AppTypeNameV2::kPluginVm:
      return kPluginVmHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowser:
      return kStandaloneBrowserHistogramName;
    case apps::AppTypeNameV2::kRemote:
      return kRemoteHistogramName;
    case apps::AppTypeNameV2::kBorealis:
      return kBorealisHistogramName;
    case apps::AppTypeNameV2::kSystemWeb:
      return kSystemWebAppHistogramName;
    case apps::AppTypeNameV2::kChromeBrowser:
      return kChromeBrowserHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserChromeApp:
      return kStandaloneBrowserChromeAppHistogramName;
    case apps::AppTypeNameV2::kExtension:
      return kExtensionHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserExtension:
      return kStandaloneBrowserExtensionHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserChromeAppWindow:
      return kStandaloneBrowserChromeAppWindowHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserChromeAppTab:
      return kStandaloneBrowserChromeAppTabHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserWebAppWindow:
      return kStandaloneBrowserWebAppWindowHistogramName;
    case apps::AppTypeNameV2::kStandaloneBrowserWebAppTab:
      return kStandaloneBrowserWebAppTabHistogramName;
    case apps::AppTypeNameV2::kBruschetta:
      return kBruschettaHistogramName;
  }
}

ApplicationInstallTime ConvertInstallTimeToProtoApplicationInstallTime(
    InstallTime install_time) {
  switch (install_time) {
    case InstallTime::kInit:
      return ApplicationInstallTime::APPLICATION_INSTALL_TIME_INIT;
    case InstallTime::kRunning:
      return ApplicationInstallTime::APPLICATION_INSTALL_TIME_RUNNING;
    default:
      return ApplicationInstallTime::APPLICATION_INSTALL_TIME_UNKNOWN;
  }
}

void RecordAppLaunchMetrics(Profile* profile,
                            AppType app_type,
                            const std::string& app_id,
                            apps::LaunchSource launch_source,
                            apps::LaunchContainer container) {
  if (app_type == AppType::kUnknown) {
    return;
  }

  RecordAppLaunchSource(launch_source);
  RecordAppLaunchPerAppType(
      GetAppTypeName(profile, app_type, app_id, container));
  RecordAppLaunchPerAppTypeV2(
      GetAppTypeNameV2(profile, app_type, app_id, container));

  // TODO(b/356937112): Refactor the metrics DemoMode.AppLaunchSource
  if (ash::DemoSession::IsDeviceInDemoMode()) {
    ash::DemoSession::AppLaunchSource source;
    bool will_report = true;
    // Apps launched from the demo mode app has the launch source of
    // kFromOtherApp, but we do not report it here since there could be other
    // places that launch apps with the same launch source of kFromOtherApp. So,
    // to be more accurate, we report it in
    // [chrome_demo_mode_app_delegate.cc]ChromeDemoModeAppDelegate::LaunchApp.
    // Additionally, we report only the following types of launch source based
    // on the need of demo mode.
    switch (launch_source) {
      case apps::LaunchSource::kFromAppListGrid:
        source = ash::DemoSession::AppLaunchSource::kAppList;
        break;
      case apps::LaunchSource::kFromAppListQuery:
        source = ash::DemoSession::AppLaunchSource::kAppListQuery;
        break;
      case apps::LaunchSource::kFromShelf:
        source = ash::DemoSession::AppLaunchSource::kShelf;
        break;
      default:
        will_report = false;
    }
    if (will_report) {
      ash::DemoSession::RecordAppLaunchSource(source);
    }
  }

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
  if (proxy && proxy->AppPlatformMetrics()) {
    proxy->AppPlatformMetrics()->RecordAppLaunchUkm(app_type, app_id,
                                                    launch_source, container);
  }
}

AppPlatformMetrics::UsageTime::UsageTime() = default;

AppPlatformMetrics::UsageTime::~UsageTime() = default;

AppPlatformMetrics::UsageTime::UsageTime(const base::Value& value) {
  const base::Value::Dict* data_dict = value.GetIfDict();
  if (!data_dict) {
    return;
  }

  const std::string* const app_id_value =
      data_dict->FindString(kUsageTimeAppIdKey);
  if (!app_id_value) {
    return;
  }

  const std::string* const app_publisher_id_value =
      data_dict->FindString(kUsageTimeAppPublisherIdKey);
  if (app_publisher_id_value) {
    app_publisher_id = *app_publisher_id_value;
  }

  const std::string* const app_type_value =
      data_dict->FindString(kUsageTimeAppTypeKey);
  if (!app_type_value) {
    return;
  }

  const std::optional<const base::TimeDelta> running_time_value =
      base::ValueToTimeDelta(data_dict->Find(kUsageTimeDurationKey));
  const std::optional<const base::TimeDelta> reporting_usage_time_value =
      base::ValueToTimeDelta(data_dict->Find(kReportingUsageTimeDurationKey));
  if (!running_time_value.has_value() &&
      !reporting_usage_time_value.has_value()) {
    return;
  }

  if (running_time_value.has_value()) {
    running_time = running_time_value.value();
  }

  if (reporting_usage_time_value.has_value()) {
    reporting_usage_time = reporting_usage_time_value.value();
  }

  app_id = *app_id_value;
  app_type_name = GetAppTypeNameFromString(*app_type_value);

  // We normally use this as we load data from the pref store at the
  // beginning of a new session which is when windows are normally closed.
  window_is_closed = true;
}

base::Value::Dict AppPlatformMetrics::UsageTime::ConvertToDict() const {
  base::Value::Dict usage_time_dict;
  usage_time_dict.Set(kUsageTimeAppIdKey, app_id);
  usage_time_dict.Set(kUsageTimeAppPublisherIdKey, app_publisher_id);
  usage_time_dict.Set(kUsageTimeAppTypeKey,
                      GetAppTypeHistogramName(app_type_name));
  usage_time_dict.Set(kUsageTimeDurationKey,
                      base::TimeDeltaToValue(running_time));
  usage_time_dict.Set(kReportingUsageTimeDurationKey,
                      base::TimeDeltaToValue(reporting_usage_time));
  return usage_time_dict;
}

AppPlatformMetrics::AppPlatformMetrics(
    Profile* profile,
    apps::AppRegistryCache& app_registry_cache,
    InstanceRegistry& instance_registry)
    : profile_(profile), app_registry_cache_(app_registry_cache) {
  app_registry_cache_observer_.Observe(&app_registry_cache);
  instance_registry_observation_.Observe(&instance_registry);
  if (chromeos::IsManagedGuestSession()) {
    CHECK(ukm::UkmRecorder::Get());
    ukm_recorder_observer_.Observe(ukm::UkmRecorder::Get());
  }
  user_type_by_device_type_ = GetUserTypeByDeviceTypeMetrics();
  InitRunningDuration();
  LoadAppsUsageTimeUkmFromPref();
  ReadInstalledApps();
}

AppPlatformMetrics::~AppPlatformMetrics() {
  UpdateMetricsBeforeShutdown();
  OnTenMinutes();

  // Notify registered observers.
  for (auto& observer : observers_) {
    observer.OnAppPlatformMetricsDestroyed();
  }
}

// static
ukm::SourceId AppPlatformMetrics::GetSourceId(Profile* profile,
                                              const std::string& app_id) {
  if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
    return ukm::kInvalidSourceId;
  }

  AppType app_type = GetAppType(profile, app_id);
  if (!ShouldRecordAppKMForAppTypeName(app_type)) {
    return ukm::kInvalidSourceId;
  }

  GURL url = GetURLForApp(profile, app_id);
  if (url.is_empty()) {
    return ukm::kInvalidSourceId;
  }

  switch (app_type) {
    case AppType::kBuiltIn:
    case AppType::kChromeApp:
    case AppType::kExtension:
    case AppType::kStandaloneBrowser:
    case AppType::kStandaloneBrowserChromeApp:
    case AppType::kStandaloneBrowserExtension:
    case AppType::kSystemWeb:
      return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(
          url, ukm::AppType::kChromeApp);
    case AppType::kArc:
      return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(url,
                                                          ukm::AppType::kArc);
    case AppType::kWeb: {
      // Some system web-apps may be PWAs.
      if (IsSystemWebApp(profile, app_id)) {
        return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(
            url, ukm::AppType::kChromeApp);
      }
      return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(url,
                                                          ukm::AppType::kPWA);
    }
    case AppType::kCrostini:
      return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(
          url, ukm::AppType::kCrostini);
    case AppType::kBorealis:
      return ukm::AppSourceUrlRecorder::GetSourceIdForUrl(
          url, ukm::AppType::kBorealis);
    // App types that are not supported by UKM.
    default:
      return ukm::kInvalidSourceId;
  }
}

// static
GURL AppPlatformMetrics::GetURLForApp(Profile* profile,
                                      const std::string& app_id) {
  AppType app_type = GetAppType(profile, app_id);

  // If the app should not be recorded, then emit an empty URL so the URL is not
  // recorded for the associated app.
  if (!ShouldRecordAppKMForAppTypeName(app_type)) {
    return GURL();
  }

  switch (app_type) {
    // |app_id| is already hashed for these apps and are of the format
    // app://{app_id}.
    case AppType::kBuiltIn:
    case AppType::kChromeApp:
    case AppType::kExtension:
    case AppType::kStandaloneBrowser:
    case AppType::kStandaloneBrowserChromeApp:
    case AppType::kStandaloneBrowserExtension:
    // For system web apps, call GetSourceIdForChromeApp to record the app
    // id because the url could be filtered by the server side.
    case AppType::kSystemWeb:
      return ukm::AppSourceUrlRecorder::GetURLForChromeApp(app_id);
    // ARC apps contain the app package name and the URL generated is of the
    // format app://{package_name}. The package name will be populated if it is
    // a properly registered ARC app.
    case AppType::kArc: {
      std::string package_name = GetPublisherId(profile, app_id);
      // Empty package name ID indicates that the ARC app is not properly
      // registered application.
      if (package_name.empty() || app_id.empty()) {
        return GURL();
      }
      return ukm::AppSourceUrlRecorder::GetURLForArcPackageName(package_name);
    }
    case AppType::kWeb: {
      // Some PWAs can be categorized as system web apps. System web apps should
      // be encoded as a ChromeApp hash.
      if (IsSystemWebApp(profile, app_id)) {
        return ukm::AppSourceUrlRecorder::GetURLForChromeApp(app_id);
      }

      std::string publisher_id = GetPublisherId(profile, app_id);
      // Empty publisher ID indicates that the app is not a properly registered
      // PWA.
      if (publisher_id.empty() || app_id.empty()) {
        return GURL();
      }

      return ukm::AppSourceUrlRecorder::GetURLForPWA(GURL(publisher_id));
    }
    case AppType::kCrostini: {
      auto crostini_app_id = GetIdForCrostini(profile, app_id);
      return ukm::AppSourceUrlRecorder::GetURLForCrostini(
          crostini_app_id.desktop_id, crostini_app_id.registration_name);
    }
    case AppType::kBorealis:
      return GetURLForBorealis(profile, app_id);
    // Other app types should not be logged. Return empty GURL so that
    // these app types are not recorded.
    default:
      return GURL();
  }
}

// static
std::string AppPlatformMetrics::GetPublisherId(Profile* profile,
                                               const std::string& app_id) {
  std::string publisher_id;
  apps::AppServiceProxyFactory::GetForProfile(profile)
      ->AppRegistryCache()
      .ForOneApp(app_id, [&publisher_id](const apps::AppUpdate& update) {
        publisher_id = update.PublisherId();
      });
  return publisher_id;
}

// static
GURL AppPlatformMetrics::GetURLForBorealis(Profile* profile,
                                           const std::string& app_id) {
  // Most Borealis apps are identified by a numeric ID, except these.
  if (app_id == borealis::kClientAppId) {
    return ukm::AppSourceUrlRecorder::GetURLForBorealis("client");
  } else if (app_id == borealis::kInstallerAppId) {
    return ukm::AppSourceUrlRecorder::GetURLForBorealis("installer");
  } else if (app_id.find(borealis::kIgnoredAppIdPrefix) != std::string::npos) {
    // These are not real apps from a user's point of view, so it doesn't make
    // sense to record metrics for them.
    return GURL();
  }

  // For most borealis apps, we convert to the "steam app id", which is a unique
  // number valve assigns to each game.
  //
  // This is more robust, as it handles some unidentified apps (if they have a
  // steam id).
  std::optional<int> borealis_id = borealis::SteamGameId(profile, app_id);
  if (borealis_id.has_value()) {
    return ukm::AppSourceUrlRecorder::GetURLForBorealis(
        base::NumberToString(borealis_id.value()));
  }

  // If there's no steam id then we're not allowed to record anything that
  // could identify the app (and we don't know the app name anyway), but
  // recording every unregistered app in one big bucket is fine.
  //
  // In general all Borealis apps should have a steam id, so if we do see this
  // Source ID being reported, that's a bug.
  LOG(WARNING) << "Couldn't get Borealis ID for UNREGISTERED app " << app_id;
  return ukm::AppSourceUrlRecorder::GetURLForBorealis("UNREGISTERED");
}

// static
CrostiniAppId AppPlatformMetrics::GetIdForCrostini(Profile* profile,
                                                   const std::string& app_id) {
  auto* registry =
      guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile);
  auto registration = registry->GetRegistration(app_id);
  if (!registration) {
    // If there's no registration then we're not allowed to record anything
    // that could identify the app (and we don't know the app name anyway),
    // but recording every unregistered app in one big bucket is fine.
    return CrostiniAppId{
        .desktop_id = "UNREGISTERED",
        .registration_name = "UNREGISTERED",
    };
  }
  auto desktop_id = registration->DesktopFileId() == ""
                        ? "NoId"
                        : registration->DesktopFileId();
  return CrostiniAppId{
      .desktop_id = desktop_id,
      .registration_name = registration->Name(),

  };
}

// static
void AppPlatformMetrics::RemoveSourceId(ukm::SourceId source_id) {
  ukm::AppSourceUrlRecorder::MarkSourceForDeletion(source_id);
}

// static
std::string AppPlatformMetrics::GetAppsCountHistogramNameForTest(
    AppTypeName app_type_name) {
  return kAppsCountHistogramPrefix + GetAppTypeHistogramName(app_type_name);
}

// static
std::string
AppPlatformMetrics::GetAppsCountPerInstallReasonHistogramNameForTest(
    AppTypeName app_type_name,
    apps::InstallReason install_reason) {
  return kAppsCountPerInstallReasonHistogramPrefix +
         GetAppTypeHistogramName(app_type_name) + "." +
         GetInstallReason(install_reason);
}

// static
std::string AppPlatformMetrics::GetAppsRunningDurationHistogramNameForTest(
    AppTypeName app_type_name) {
  return kAppsRunningDurationHistogramPrefix +
         GetAppTypeHistogramName(app_type_name);
}

// static
std::string AppPlatformMetrics::GetAppsRunningPercentageHistogramNameForTest(
    AppTypeName app_type_name) {
  return kAppsRunningPercentageHistogramPrefix +
         GetAppTypeHistogramName(app_type_name);
}

// static
std::string AppPlatformMetrics::GetAppsActivatedCountHistogramNameForTest(
    AppTypeName app_type_name) {
  return kAppsActivatedCountHistogramPrefix +
         GetAppTypeHistogramName(app_type_name);
}

std::string AppPlatformMetrics::GetAppsUsageTimeHistogramNameForTest(
    AppTypeName app_type_name) {
  return kAppsUsageTimeHistogramPrefix + GetAppTypeHistogramName(app_type_name);
}

std::string AppPlatformMetrics::GetAppsUsageTimeHistogramNameForTest(
    AppTypeNameV2 app_type_name) {
  return kAppsUsageTimeHistogramPrefixV2 +
         GetAppTypeHistogramNameV2(app_type_name);
}

void AppPlatformMetrics::OnNewDay() {
  should_record_metrics_on_new_day_ = true;
  RecordAppsCount(AppType::kUnknown);
  RecordAppsRunningDuration();
}

void AppPlatformMetrics::OnTenMinutes() {
  if (should_refresh_activated_count_pref) {
    should_refresh_activated_count_pref = false;
    ScopedDictPrefUpdate activated_count_update(profile_->GetPrefs(),
                                                kAppActivatedCount);
    for (auto it : activated_count_) {
      std::string app_type_name = GetAppTypeHistogramName(it.first);
      DCHECK(!app_type_name.empty());
      activated_count_update->Set(app_type_name, it.second);
    }
  }

  if (should_refresh_duration_pref) {
    should_refresh_duration_pref = false;
    ScopedDictPrefUpdate running_duration_update(profile_->GetPrefs(),
                                                 kAppRunningDuration);
    for (auto it : running_duration_) {
      std::string app_type_name = GetAppTypeHistogramName(it.first);
      DCHECK(!app_type_name.empty());
      running_duration_update->SetByDottedPath(
          app_type_name, base::TimeDeltaToValue(it.second));
    }
  }
}

void AppPlatformMetrics::OnFiveMinutes() {
  // If there is app usage time loaded from the user pref for previous login,
  // record the AppKM.
  if (!usage_times_from_pref_.empty()) {
    RecordAppsUsageTimeUkmFromPref();
    usage_times_from_pref_.clear();
  }
  RecordAppsUsageTime();
  SaveUsageTime();
}

void AppPlatformMetrics::OnTwoHours() {
  RecordAppsUsageTimeUkm();
}

void AppPlatformMetrics::RecordAppLaunchUkm(AppType app_type,
                                            const std::string& app_id,
                                            apps::LaunchSource launch_source,
                                            apps::LaunchContainer container) {
  // We do not tie observers to local app sync settings, so we notify them
  // first. It is the responsibility of observers to enforce appropriate checks
  // and restrictions with something appropriate before using this data.
  for (auto& observer : observers_) {
    observer.OnAppLaunched(app_id, app_type, launch_source);
  }

  if (app_type == AppType::kUnknown ||
      !ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(), app_id)) {
    return;
  }

  apps::AppTypeName app_type_name =
      GetAppTypeName(profile_, app_type, app_id, container);

  ukm::SourceId source_id = GetSourceId(profile_, app_id);
  if (source_id == ukm::kInvalidSourceId) {
    return;
  }

  ukm::builders::ChromeOSApp_Launch builder(source_id);
  builder.SetAppType((int)app_type_name)
      .SetLaunchSource((int)launch_source)
      .SetUserDeviceMatrix(GetUserTypeByDeviceTypeMetrics())
      .Record(ukm::UkmRecorder::Get());
  RemoveSourceId(source_id);
}

void AppPlatformMetrics::RecordAppUninstallUkm(
    AppType app_type,
    const std::string& app_id,
    UninstallSource uninstall_source) {
  // We do not tie observers to local app sync settings, so we notify them
  // first. It is the responsibility of observers to enforce appropriate checks
  // and restrictions with something appropriate before using this data.
  for (auto& observer : observers_) {
    observer.OnAppUninstalled(app_id, app_type, uninstall_source);
  }
  if (!ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(), app_id)) {
    return;
  }

  AppTypeName app_type_name = GetAppTypeName(
      profile_, app_type, app_id, apps::LaunchContainer::kLaunchContainerNone);

  ukm::SourceId source_id = GetSourceId(profile_, app_id);
  if (source_id == ukm::kInvalidSourceId) {
    return;
  }

  ukm::builders::ChromeOSApp_UninstallApp builder(source_id);
  builder.SetAppType((int)app_type_name)
      .SetUninstallSource((int)uninstall_source)
      .SetUserDeviceMatrix(user_type_by_device_type_)
      .Record(ukm::UkmRecorder::Get());
  RemoveSourceId(source_id);
}

void AppPlatformMetrics::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void AppPlatformMetrics::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void AppPlatformMetrics::OnAppTypeInitialized(AppType app_type) {
  if (should_record_metrics_on_new_day_) {
    RecordAppsCount(app_type);
  }
}

void AppPlatformMetrics::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
}

void AppPlatformMetrics::OnAppUpdate(const apps::AppUpdate& update) {
  if (!update.ReadinessChanged() ||
      update.Readiness() != apps::Readiness::kReady ||
      apps_util::IsInstalled(update.PriorReadiness())) {
    return;
  }

  InstallTime install_time =
      app_registry_cache_->IsAppTypeInitialized(update.AppType())
          ? InstallTime::kRunning
          : InstallTime::kInit;

  // We do not tie observers to local app sync settings, so we notify them
  // first. It is the responsibility of observers to enforce appropriate checks
  // and restrictions with something appropriate before using this data.
  for (auto& observer : observers_) {
    observer.OnAppInstalled(update.AppId(), update.AppType(),
                            update.InstallSource(), update.InstallReason(),
                            install_time);
  }

  if (!ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(),
                                 update.AppId())) {
    return;
  }

  RecordAppsInstallUkm(update, install_time);
}

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

  auto app_id = update.AppId();
  auto app_type = GetAppType(profile_, app_id);
  if (app_type == AppType::kUnknown) {
    return;
  }

  bool is_active = update.State() & apps::InstanceState::kActive;
  if (is_active) {
    AppTypeName app_type_name =
        GetAppTypeNameForWindow(profile_, app_type, app_id, update.Window());
    if (app_type_name == apps::AppTypeName::kUnknown) {
      return;
    }

    apps::InstanceState kInActivated = static_cast<apps::InstanceState>(
        apps::InstanceState::kVisible | apps::InstanceState::kRunning);

    // For the browser window, if a tab of the browser is activated, we don't
    // need to calculate the browser window running time.
    if ((app_id == app_constants::kChromeAppId ||
         app_id == app_constants::kLacrosAppId) &&
        browser_to_tab_list_.HasActivatedTab(update.Window())) {
      SetWindowInActivated(app_id, update.InstanceId(), kInActivated);
      return;
    }

    // For web apps open in tabs, set the top browser window as inactive to stop
    // calculating the browser window running time.
    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.
      auto* browser_window =
          app_type_name == apps::AppTypeName::kStandaloneBrowser
              ? update.Window()
              : update.Window()->GetToplevelWindow();
      browser_to_tab_list_.RemoveActivatedTab(update.InstanceId());
      browser_to_tab_list_.AddActivatedTab(browser_window, update.InstanceId(),
                                           update.AppId());
      InstanceState state;
      base::UnguessableToken browser_id;
      std::string browser_app_id;
      GetBrowserInstanceInfo(browser_window, browser_id, browser_app_id, state);
      if (browser_id) {
        SetWindowInActivated(browser_app_id, browser_id, kInActivated);
      }
    }

    AppTypeNameV2 app_type_name_v2 =
        GetAppTypeNameV2(profile_, app_type, app_id, update.Window());

    SetWindowActivated(app_type, app_type_name, app_type_name_v2, app_id,
                       update.InstanceId());
    return;
  }

  AppTypeName app_type_name = AppTypeName::kUnknown;
  auto it = running_start_time_.find(update.InstanceId());
  if (it != running_start_time_.end()) {
    app_type_name = it->second.app_type_name;
  }

  if (IsAppOpenedInTab(app_type_name, app_id)) {
    UpdateBrowserWindowStatus(update);
  }

  SetWindowInActivated(app_id, update.InstanceId(), update.State());
}

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

void AppPlatformMetrics::OnStartingShutdown() {
  CHECK(chromeos::IsManagedGuestSession());
  UpdateMetricsBeforeShutdown();
  RecordAppsUsageTimeUkm();
}

void AppPlatformMetrics::GetBrowserInstanceInfo(
    const aura::Window* browser_window,
    base::UnguessableToken& browser_id,
    std::string& browser_app_id,
    InstanceState& state) const {
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
  DCHECK(proxy);
  browser_id = base::UnguessableToken();
  browser_app_id = std::string();
  state = InstanceState::kUnknown;
  proxy->InstanceRegistry().ForInstancesWithWindow(
      browser_window, [&](const InstanceUpdate& browser_update) {
        if (browser_update.AppId() == app_constants::kChromeAppId ||
            browser_update.AppId() == app_constants::kLacrosAppId) {
          browser_id = browser_update.InstanceId();
          browser_app_id = browser_update.AppId();
          state = browser_update.State();
        }
      });
}

void AppPlatformMetrics::UpdateBrowserWindowStatus(
    const InstanceUpdate& update) {
  const base::UnguessableToken& tab_id = update.InstanceId();
  const auto* browser_window = browser_to_tab_list_.GetBrowserWindow(tab_id);
  if (!browser_window) {
    return;
  }

  // Remove the tab id from `active_browser_to_tabs_`.
  browser_to_tab_list_.RemoveActivatedTab(tab_id);

  // If there are other activated web app tab, we don't need to set the browser
  // window as activated.
  if (browser_to_tab_list_.HasActivatedTab(browser_window)) {
    return;
  }

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
  DCHECK(proxy);
  InstanceState state;
  base::UnguessableToken browser_id;
  std::string browser_app_id;
  GetBrowserInstanceInfo(browser_window, browser_id, browser_app_id, state);
  if (state & InstanceState::kActive) {
    AppType app_type = AppType::kChromeApp;
    AppTypeName app_type_name = AppTypeName::kChromeBrowser;
    AppTypeNameV2 app_type_name_v2 = AppTypeNameV2::kChromeBrowser;
    if (browser_app_id == app_constants::kLacrosAppId) {
      app_type = AppType::kStandaloneBrowser;
      app_type_name = AppTypeName::kStandaloneBrowser;
      app_type_name_v2 = AppTypeNameV2::kStandaloneBrowser;
    }
    // The browser window is activated, start calculating the browser window
    // running time.
    SetWindowActivated(app_type, app_type_name, app_type_name_v2,
                       browser_app_id, browser_id);
  }
}

void AppPlatformMetrics::SetWindowActivated(
    AppType app_type,
    AppTypeName app_type_name,
    AppTypeNameV2 app_type_name_v2,
    const std::string& app_id,
    const base::UnguessableToken& instance_id) {
  auto it = running_start_time_.find(instance_id);
  if (it != running_start_time_.end()) {
    return;
  }

  running_start_time_[instance_id].start_time = base::TimeTicks::Now();
  running_start_time_[instance_id].app_type_name = app_type_name;
  running_start_time_[instance_id].app_type_name_v2 = app_type_name_v2;

  ++activated_count_[app_type_name];
  should_refresh_activated_count_pref = true;

  start_time_per_five_minutes_[instance_id].start_time = base::TimeTicks::Now();
  start_time_per_five_minutes_[instance_id].app_type_name = app_type_name;
  start_time_per_five_minutes_[instance_id].app_type_name_v2 = app_type_name_v2;
  start_time_per_five_minutes_[instance_id].app_id = app_id;
}

void AppPlatformMetrics::SetWindowInActivated(
    const std::string& app_id,
    const base::UnguessableToken& instance_id,
    apps::InstanceState state) {
  bool is_close = state & apps::InstanceState::kDestroyed;
  auto two_hours_it = usage_time_per_two_hours_.find(instance_id);
  if (is_close && two_hours_it != usage_time_per_two_hours_.end()) {
    two_hours_it->second.window_is_closed = true;
  }

  auto it = running_start_time_.find(instance_id);
  if (it == running_start_time_.end()) {
    return;
  }

  AppTypeName app_type_name = it->second.app_type_name;
  AppTypeNameV2 app_type_name_v2 = it->second.app_type_name_v2;

  running_duration_[app_type_name] +=
      base::TimeTicks::Now() - it->second.start_time;

  base::TimeDelta running_time =
      base::TimeTicks::Now() -
      start_time_per_five_minutes_[instance_id].start_time;
  app_type_running_time_per_five_minutes_[app_type_name] += running_time;
  app_type_v2_running_time_per_five_minutes_[app_type_name_v2] += running_time;

  UpdateUsageTime(instance_id, app_id, app_type_name, running_time);

  running_start_time_.erase(it);
  start_time_per_five_minutes_.erase(instance_id);

  should_refresh_duration_pref = true;
}

void AppPlatformMetrics::InitRunningDuration() {
  ScopedDictPrefUpdate running_duration_update(profile_->GetPrefs(),
                                               kAppRunningDuration);
  ScopedDictPrefUpdate activated_count_update(profile_->GetPrefs(),
                                              kAppActivatedCount);

  for (auto app_type_name : kAppTypeNameSet) {
    std::string key = GetAppTypeHistogramName(app_type_name);
    if (key.empty()) {
      continue;
    }

    std::optional<base::TimeDelta> unreported_duration =
        base::ValueToTimeDelta(running_duration_update->FindByDottedPath(key));
    if (unreported_duration.has_value()) {
      running_duration_[app_type_name] = unreported_duration.value();
    }

    std::optional<int> count = activated_count_update->FindIntByDottedPath(key);
    if (count.has_value()) {
      activated_count_[app_type_name] = count.value();
    }
  }
}

void AppPlatformMetrics::ClearRunningDuration() {
  running_duration_.clear();
  activated_count_.clear();

  profile_->GetPrefs()->SetDict(kAppRunningDuration, base::Value::Dict());
  profile_->GetPrefs()->SetDict(kAppActivatedCount, base::Value::Dict());
}

void AppPlatformMetrics::ReadInstalledApps() {
  app_registry_cache_->ForEachApp([this](const apps::AppUpdate& update) {
    if (ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(),
                                  update.AppId())) {
      RecordAppsInstallUkm(update, InstallTime::kInit);
    }
  });
}

void AppPlatformMetrics::RecordAppsCount(AppType app_type) {
  std::map<AppTypeName, int> app_count;
  std::map<AppTypeName, std::map<apps::InstallReason, int>>
      app_count_per_install_reason;

  app_registry_cache_->ForEachApp(
      [app_type, this, &app_count,
       &app_count_per_install_reason](const apps::AppUpdate& update) {
        if (app_type != apps::AppType::kUnknown &&
            (update.AppType() != app_type ||
             update.AppId() == app_constants::kChromeAppId)) {
          return;
        }

        AppTypeName app_type_name =
            GetAppTypeName(profile_, update.AppType(), update.AppId(),
                           apps::LaunchContainer::kLaunchContainerNone);

        if (app_type_name == AppTypeName::kChromeBrowser ||
            app_type_name == AppTypeName::kUnknown) {
          return;
        }

        ++app_count[app_type_name];
        ++app_count_per_install_reason[app_type_name][update.InstallReason()];
      });

  for (auto it : app_count) {
    std::string histogram_name = GetAppTypeHistogramName(it.first);
    if (!histogram_name.empty() &&
        histogram_name != kChromeBrowserHistogramName) {
      // If there are more than a thousand apps installed, then that count is
      // going into an overflow bucket. We don't expect this scenario to happen
      // often.
      base::UmaHistogramCounts1000(kAppsCountHistogramPrefix + histogram_name,
                                   it.second);
      for (auto install_reason_it : app_count_per_install_reason[it.first]) {
        base::UmaHistogramCounts1000(
            kAppsCountPerInstallReasonHistogramPrefix + histogram_name + "." +
                GetInstallReason(install_reason_it.first),
            install_reason_it.second);
      }
    }
  }
}

void AppPlatformMetrics::RecordAppsRunningDuration() {
  for (auto& it : running_start_time_) {
    running_duration_[it.second.app_type_name] +=
        GetDurationAndResetStartTime(it.second.start_time);
  }

  base::TimeDelta total_running_duration;
  for (auto it : running_duration_) {
    base::UmaHistogramCustomTimes(
        kAppsRunningDurationHistogramPrefix + GetAppTypeHistogramName(it.first),
        it.second, kMinDuration, kMaxDuration, kDurationBuckets);
    total_running_duration += it.second;
  }

  if (!total_running_duration.is_zero()) {
    for (auto it : running_duration_) {
      base::UmaHistogramPercentage(kAppsRunningPercentageHistogramPrefix +
                                       GetAppTypeHistogramName(it.first),
                                   100 * (it.second / total_running_duration));
    }
  }

  for (auto it : activated_count_) {
    base::UmaHistogramCounts10000(
        kAppsActivatedCountHistogramPrefix + GetAppTypeHistogramName(it.first),
        it.second);
  }

  ClearRunningDuration();
}

void AppPlatformMetrics::RecordAppsUsageTime() {
  for (auto& it : start_time_per_five_minutes_) {
    base::TimeDelta running_time =
        GetDurationAndResetStartTime(it.second.start_time);
    app_type_running_time_per_five_minutes_[it.second.app_type_name] +=
        running_time;
    app_type_v2_running_time_per_five_minutes_[it.second.app_type_name_v2] +=
        running_time;
    UpdateUsageTime(it.first, it.second.app_id, it.second.app_type_name,
                    running_time);
  }

  for (auto it : app_type_running_time_per_five_minutes_) {
    base::UmaHistogramCustomTimes(
        kAppsUsageTimeHistogramPrefix + GetAppTypeHistogramName(it.first),
        it.second, kMinDuration, kMaxUsageDuration, kUsageTimeBuckets);
  }

  for (auto it : app_type_v2_running_time_per_five_minutes_) {
    base::UmaHistogramCustomTimes(
        kAppsUsageTimeHistogramPrefixV2 + GetAppTypeHistogramNameV2(it.first),
        it.second, kMinDuration, kMaxUsageDuration, kUsageTimeBuckets);
  }

  app_type_running_time_per_five_minutes_.clear();
  app_type_v2_running_time_per_five_minutes_.clear();
}

void AppPlatformMetrics::RecordAppsUsageTimeUkm() {
  if (!ShouldRecordAppKM(profile_)) {
    // Attempt to clean up pre-existing data in the pref store. This is useful
    // (and harmless) because we routinely clean up usage data that has already
    // been reported.
    CleanUpAppsUsageInfoInPrefStore();
    return;
  }

  std::vector<base::UnguessableToken> closed_instance_ids;
  for (auto& it : usage_time_per_two_hours_) {
    apps::AppTypeName app_type_name = it.second.app_type_name;
    ukm::SourceId source_id = it.second.source_id;
    DCHECK_NE(source_id, ukm::kInvalidSourceId);
    if (!it.second.running_time.is_zero()) {
      if (ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(),
                                    it.second.app_id)) {
        auto new_source_id = GetSourceId(profile_, it.second.app_id);
        if (new_source_id != ukm::kInvalidSourceId) {
          ukm::builders::ChromeOSApp_UsageTime builder(new_source_id);
          builder.SetAppType((int)it.second.app_type_name)
              .SetDuration(it.second.running_time.InMilliseconds())
              .SetUserDeviceMatrix(user_type_by_device_type_)
              .Record(ukm::UkmRecorder::Get());
          RemoveSourceId(new_source_id);
        }

        // Preserve a copy of UsageTime UKM to investigate the null app id
        // issue.
        ukm::builders::ChromeOSApp_UsageTimeReusedSourceId builder(source_id);
        builder.SetAppType((int)app_type_name)
            .SetDuration(it.second.running_time.InMilliseconds())
            .SetUserDeviceMatrix(user_type_by_device_type_)
            .Record(ukm::UkmRecorder::Get());
      }

      // UMA for Mall app.
      if (AppIdToName(it.second.app_id) == DefaultAppName::kMall) {
        base::UmaHistogramCustomTimes("Apps.AppDiscovery.MallUsageTime",
                                      it.second.running_time, base::Seconds(1),
                                      base::Hours(2), 100);
      }

      // Also reset time in the pref store now that we have reported this data.
      ClearAppsUsageTimeForInstance(it.first.ToString());
    }
    if (it.second.window_is_closed) {
      closed_instance_ids.push_back(it.first);
      RemoveSourceId(source_id);
    } else {
      it.second.running_time = base::TimeDelta();
    }
  }

  // `usage_time_per_two_hours_` can't be cleared to reuse the source id for
  // open windows. So only closed window records can be deleted from
  // `usage_time_per_two_hours_`.
  for (const auto& instance_id : closed_instance_ids) {
    usage_time_per_two_hours_.erase(instance_id);
  }

  CleanUpAppsUsageInfoInPrefStore();
}

void AppPlatformMetrics::RecordAppsInstallUkm(const apps::AppUpdate& update,
                                              InstallTime install_time) {
  AppTypeName app_type_name =
      GetAppTypeName(profile_, update.AppType(), update.AppId(),
                     apps::LaunchContainer::kLaunchContainerNone);

  ukm::SourceId source_id = GetSourceId(profile_, update.AppId());
  if (source_id == ukm::kInvalidSourceId) {
    return;
  }

  ukm::builders::ChromeOSApp_InstalledApp builder(source_id);
  builder.SetAppType((int)app_type_name)
      .SetInstallReason((int)update.InstallReason())
      .SetInstallSource2((int)update.InstallSource())
      .SetInstallTime((int)install_time)
      .SetUserDeviceMatrix(user_type_by_device_type_)
      .Record(ukm::UkmRecorder::Get());
  RemoveSourceId(source_id);
}

void AppPlatformMetrics::UpdateUsageTime(
    const base::UnguessableToken& instance_id,
    const std::string& app_id,
    AppTypeName app_type_name,
    const base::TimeDelta& running_time) {
  // Notify registered observers.
  for (auto& observer : observers_) {
    observer.OnAppUsage(app_id, GetAppType(profile_, app_id), instance_id,
                        running_time);
  }
  if (!ShouldRecordAppKM(profile_)) {
    // Avoid incrementing app usage counters if it cannot be reported. This
    // ensures we only track usage for the period the user has sync enabled.
    return;
  }

  auto usage_time_it = usage_time_per_two_hours_.find(instance_id);
  if (usage_time_it == usage_time_per_two_hours_.end()) {
    auto source_id = GetSourceId(profile_, app_id);
    if (source_id != ukm::kInvalidSourceId) {
      usage_time_per_two_hours_[instance_id].source_id = source_id;
      usage_time_per_two_hours_[instance_id].app_id = app_id;
      usage_time_it = usage_time_per_two_hours_.find(instance_id);
    }
  }
  if (usage_time_it != usage_time_per_two_hours_.end()) {
    usage_time_it->second.app_type_name = app_type_name;
    usage_time_it->second.running_time += running_time;
  }
}

void AppPlatformMetrics::SaveUsageTime() {
  if (!ShouldRecordAppKM(profile_)) {
    // Do not persist usage data to the pref store if it cannot be reported.
    // This will prevent unnecessary disk space usage.
    return;
  }

  ScopedDictPrefUpdate usage_dict_pref(profile_->GetPrefs(), kAppUsageTime);
  for (const auto& it : usage_time_per_two_hours_) {
    const std::string& instance_id = it.first.ToString();
    auto* const usage_info = usage_dict_pref->FindDictByDottedPath(instance_id);
    if (!usage_info) {
      // No entry in the pref store for this instance, so we create a new one.
      usage_dict_pref->SetByDottedPath(instance_id, it.second.ConvertToDict());
      continue;
    }

    // Only override the fields tracked by this component so we do not override
    // the reporting usage time.
    usage_info->Set(kUsageTimeAppIdKey, it.second.app_id);
    usage_info->Set(kUsageTimeAppTypeKey,
                    GetAppTypeHistogramName(it.second.app_type_name));
    usage_info->Set(kUsageTimeDurationKey,
                    base::TimeDeltaToValue(it.second.running_time));
  }
}

void AppPlatformMetrics::LoadAppsUsageTimeUkmFromPref() {
  const base::Value::Dict& usage_time_dict =
      profile_->GetPrefs()->GetDict(kAppUsageTime);

  for (auto it : usage_time_dict) {
    std::unique_ptr<UsageTime> usage_time =
        std::make_unique<UsageTime>(it.second);
    if (!usage_time->running_time.is_zero()) {
      usage_times_from_pref_.push_back(std::move(usage_time));
    }
  }
}

void AppPlatformMetrics::RecordAppsUsageTimeUkmFromPref() {
  if (!ShouldRecordAppKM(profile_) || usage_times_from_pref_.empty()) {
    return;
  }

  for (auto& it : usage_times_from_pref_) {
    if (ShouldRecordAppKMForAppId(profile_, app_registry_cache_.get(),
                                  it->app_id)) {
      auto source_id = GetSourceId(profile_, it->app_id);
      if (source_id != ukm::kInvalidSourceId) {
        ukm::builders::ChromeOSApp_UsageTime builder(source_id);
        builder.SetAppType((int)it->app_type_name)
            .SetDuration(it->running_time.InMilliseconds())
            .SetUserDeviceMatrix(user_type_by_device_type_)
            .Record(ukm::UkmRecorder::Get());
        RemoveSourceId(source_id);
      }

      // All windows read from the user pref have been closed before login, so
      // create a new source id here, since we don't have previous source ids
      // for them. This UKM record should not have the null app id issue. Still
      // preserve a copy of UsageTime UKM to investigate the null app id issue
      // for consistency.
      source_id = GetSourceId(profile_, it->app_id);
      if (source_id != ukm::kInvalidSourceId) {
        ukm::builders::ChromeOSApp_UsageTimeReusedSourceId builder(source_id);
        builder.SetAppType((int)it->app_type_name)
            .SetDuration(it->running_time.InMilliseconds())
            .SetUserDeviceMatrix(user_type_by_device_type_)
            .Record(ukm::UkmRecorder::Get());
        RemoveSourceId(source_id);
      }
    }

    // Clear app UKM usage from the pref store now that we have reported this
    // data.
    for (const auto usage_it : profile_->GetPrefs()->GetDict(kAppUsageTime)) {
      if (CHECK_DEREF(usage_it.second.GetDict().FindString(
              kUsageTimeAppIdKey)) == it->app_id) {
        ClearAppsUsageTimeForInstance(usage_it.first);
      }
    }
  }
}

void AppPlatformMetrics::CleanUpAppsUsageInfoInPrefStore() {
  ScopedDictPrefUpdate usage_time_pref_update(profile_->GetPrefs(),
                                              kAppUsageTime);
  auto usage_it = usage_time_pref_update->begin();
  while (usage_it != usage_time_pref_update->end()) {
    UsageTime usage_time(usage_it->second);
    if (usage_time.reporting_usage_time.is_zero() &&
        usage_time.running_time.is_zero()) {
      usage_it = usage_time_pref_update->erase(usage_it);
      continue;
    }
    usage_it++;
  }
}

void AppPlatformMetrics::ClearAppsUsageTimeForInstance(
    std::string_view instance_id) {
  ScopedDictPrefUpdate usage_time_pref_update(profile_->GetPrefs(),
                                              kAppUsageTime);
  auto* instance_dict =
      usage_time_pref_update->FindDictByDottedPath(instance_id);
  if (instance_dict) {
    instance_dict->Set(kUsageTimeDurationKey, base::Int64ToValue(0));
  }
}

void AppPlatformMetrics::UpdateMetricsBeforeShutdown() {
  for (auto& it : running_start_time_) {
    running_duration_[it.second.app_type_name] +=
        GetDurationAndResetStartTime(it.second.start_time);
  }

  RecordAppsUsageTime();
}

}  // namespace apps