chromium/chrome/browser/ash/policy/reporting/metrics_reporting/apps/app_events_observer_unittest.cc

// Copyright 2022 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/ash/policy/reporting/metrics_reporting/apps/app_events_observer.h"

#include <memory>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_result_type.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics_service_test_base.h"
#include "chrome/browser/apps/app_service/publishers/app_publisher.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/apps/app_platform_metrics_retriever.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/metric_reporting_prefs.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/reporting/metrics/fakes/fake_reporting_settings.h"
#include "components/reporting/proto/synced/metric_data.pb.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/protos/app_types.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::Contains;
using ::testing::Eq;
using ::testing::StrEq;

namespace reporting {
namespace {

constexpr char kTestAppId[] = "TestApp";
constexpr char kTestAppPublisherId[] = "com.google.test";
constexpr char kAppInstallTrackerDiskConsumptionMetric[] =
    "Browser.ERP.AppInstallTrackerDiskConsumption";

// Fake `AppPublisher` used by the test to simulate app launches.
class FakePublisher : public ::apps::AppPublisher {
 public:
  FakePublisher(::apps::AppServiceProxy* proxy, ::apps::AppType app_type)
      : ::apps::AppPublisher(proxy) {
    RegisterPublisher(app_type);
  }

  MOCK_METHOD(void,
              Launch,
              (const std::string& app_id,
               int32_t event_flags,
               ::apps::LaunchSource launch_source,
               ::apps::WindowInfoPtr window_info));

  MOCK_METHOD(void,
              LaunchAppWithParams,
              (::apps::AppLaunchParams && params,
               ::apps::LaunchCallback callback));

  MOCK_METHOD(void,
              LoadIcon,
              (const std::string& app_id,
               const ::apps::IconKey& icon_key,
               ::apps::IconType icon_type,
               int32_t size_hint_in_dip,
               bool allow_placeholder_icon,
               ::apps::LoadIconCallback callback));
};

// Mock retriever for the `AppPlatformMetrics` component.
class MockAppPlatformMetricsRetriever : public AppPlatformMetricsRetriever {
 public:
  MockAppPlatformMetricsRetriever() : AppPlatformMetricsRetriever(nullptr) {}
  MockAppPlatformMetricsRetriever(const MockAppPlatformMetricsRetriever&) =
      delete;
  MockAppPlatformMetricsRetriever& operator=(
      const MockAppPlatformMetricsRetriever&) = delete;
  ~MockAppPlatformMetricsRetriever() override = default;

  MOCK_METHOD(void,
              GetAppPlatformMetrics,
              (AppPlatformMetricsCallback callback),
              (override));
};

class AppEventsObserverTest : public ::apps::AppPlatformMetricsServiceTestBase,
                              public testing::WithParamInterface<bool> {
 protected:
  void SetUp() override {
    ::apps::AppPlatformMetricsServiceTestBase::SetUp();

    // Disable sync so we disable UKM reporting and eliminate noise for testing
    // purposes.
    sync_service()->SetAllowedByEnterprisePolicy(false);

    // Set up `AppEventsObserver` with relevant test params.
    auto mock_app_platform_metrics_retriever =
        std::make_unique<MockAppPlatformMetricsRetriever>();
    EXPECT_CALL(*mock_app_platform_metrics_retriever, GetAppPlatformMetrics(_))
        .WillOnce([this](AppPlatformMetricsRetriever::AppPlatformMetricsCallback
                             callback) {
          std::move(callback).Run(
              app_platform_metrics_service()->AppPlatformMetrics());
        });
    app_events_observer_ = AppEventsObserver::CreateForTest(
        profile(), std::move(mock_app_platform_metrics_retriever),
        &reporting_settings_);

    InitAppType(::apps::AppType::kArc);

    // Pre-install app so it can be used by tests. Initialized after the event
    // observer to simplify testing. Scenarios that test app installs across
    // sessions are covered by browser tests.
    InstallOneApp(kTestAppId, ::apps::AppType::kArc, kTestAppPublisherId,
                  ::apps::Readiness::kReady, ::apps::InstallSource::kPlayStore);
      histogram_tester().ExpectTotalCount(
          kAppInstallTrackerDiskConsumptionMetric, 1);
  }

  void TearDown() override {
    app_events_observer_.reset();
    ::apps::AppPlatformMetricsServiceTestBase::TearDown();
  }

  void SetAllowedAppReportingTypes(const std::vector<std::string>& app_types) {
    base::Value::List allowed_app_types;
    for (const auto& app_type : app_types) {
      allowed_app_types.Append(app_type);
    }
    reporting_settings_.SetList(::ash::reporting::kReportAppInventory,
                                std::move(allowed_app_types));

    // Simulate policy update.
    bool is_app_reporting_enabled = !app_types.empty();
    app_events_observer_->SetReportingEnabled(is_app_reporting_enabled);
  }

  void InitAppType(::apps::AppType app_type) {
    ::apps::AppServiceProxyFactory::GetForProfile(profile())->OnApps(
        std::vector<::apps::AppPtr>{}, app_type,
        /*should_notify_initialized=*/true);
  }

  test::FakeReportingSettings reporting_settings_;
  std::unique_ptr<AppEventsObserver> app_events_observer_;
};

TEST_F(AppEventsObserverTest, OnAppInstalled) {
  InitAppType(::apps::AppType::kStandaloneBrowser);
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryBrowser});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Install new app.
  static constexpr char kAppId[] = "TestNewApp";
  InstallOneApp(kAppId, ::apps::AppType::kStandaloneBrowser,
                /*publisher_id=*/"", ::apps::Readiness::kReady,
                ::apps::InstallSource::kBrowser);

  // Verify data being reported.
  const MetricData& result = test_future.Take();
  ASSERT_TRUE(result.has_event_data());
  EXPECT_THAT(result.event_data().type(), Eq(MetricEventType::APP_INSTALLED));
  ASSERT_TRUE(result.has_telemetry_data());
  ASSERT_TRUE(result.telemetry_data().has_app_telemetry());
  ASSERT_TRUE(result.telemetry_data().app_telemetry().has_app_install_data());

  const AppInstallData& app_install_data =
      result.telemetry_data().app_telemetry().app_install_data();
  EXPECT_THAT(app_install_data.app_id(), StrEq(kAppId));
  EXPECT_THAT(app_install_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_STANDALONE_BROWSER));
  EXPECT_THAT(
      app_install_data.app_install_reason(),
      Eq(::apps::ApplicationInstallReason::APPLICATION_INSTALL_REASON_USER));
  EXPECT_THAT(
      app_install_data.app_install_source(),
      Eq(::apps::ApplicationInstallSource::APPLICATION_INSTALL_SOURCE_BROWSER));
  EXPECT_THAT(
      app_install_data.app_install_time(),
      Eq(::apps::ApplicationInstallTime::APPLICATION_INSTALL_TIME_RUNNING));

  // Also verify that the app install is being tracked.
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kAppId).Times(1));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppInstalled_UnsetPolicy) {
  InitAppType(::apps::AppType::kStandaloneBrowser);

  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Install new app.
  static constexpr char kAppId[] = "TestNewApp";
  InstallOneApp(kAppId, ::apps::AppType::kStandaloneBrowser,
                /*publisher_id=*/"", ::apps::Readiness::kReady,
                ::apps::InstallSource::kBrowser);

  // Verify no data is being reported and the app install is being tracked.
  ASSERT_FALSE(test_future.IsReady());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kAppId).Times(1));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppInstalled_DisallowedAppType) {
  InitAppType(::apps::AppType::kStandaloneBrowser);

  // Set policy to enable reporting for a different app type than the one being
  // tested.
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Install new app.
  static constexpr char kAppId[] = "TestNewApp";
  InstallOneApp(kAppId, ::apps::AppType::kStandaloneBrowser,
                /*publisher_id=*/"", ::apps::Readiness::kReady,
                ::apps::InstallSource::kBrowser);

  // Verify no data is being reported and the app install is being tracked.
  ASSERT_FALSE(test_future.IsReady());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kAppId).Times(1));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppInstalledWithPublisherId) {
  InitAppType(::apps::AppType::kStandaloneBrowser);

  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Install new app.
  static constexpr char kNewAppId[] = "TestNewApp";
  static constexpr char kNewAppPublisherId[] = "com.google.TestNew";
  InstallOneApp(kNewAppId, ::apps::AppType::kArc, kNewAppPublisherId,
                ::apps::Readiness::kReady, ::apps::InstallSource::kBrowser);

  // Verify data being reported.
  const MetricData& result = test_future.Take();
  ASSERT_TRUE(result.has_event_data());
  EXPECT_THAT(result.event_data().type(), Eq(MetricEventType::APP_INSTALLED));
  ASSERT_TRUE(result.has_telemetry_data());
  ASSERT_TRUE(result.telemetry_data().has_app_telemetry());
  ASSERT_TRUE(result.telemetry_data().app_telemetry().has_app_install_data());

  const AppInstallData& app_install_data =
      result.telemetry_data().app_telemetry().app_install_data();
  EXPECT_THAT(app_install_data.app_id(), StrEq(kNewAppPublisherId));
  EXPECT_THAT(app_install_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_ARC));
  EXPECT_THAT(
      app_install_data.app_install_reason(),
      Eq(::apps::ApplicationInstallReason::APPLICATION_INSTALL_REASON_USER));
  EXPECT_THAT(
      app_install_data.app_install_source(),
      Eq(::apps::ApplicationInstallSource::APPLICATION_INSTALL_SOURCE_BROWSER));
  EXPECT_THAT(
      app_install_data.app_install_time(),
      Eq(::apps::ApplicationInstallTime::APPLICATION_INSTALL_TIME_RUNNING));

  // Also verify the app install is being tracked.
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kNewAppId).Times(1));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppInstalled_PreinstalledApp) {
  static constexpr char kAppId[] = "TestNewApp";
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryBrowser});

    // Directly track new app install in pref store. We cannot use the app
    // service to simulate app install here because observers are only notified
    // of new app installs only based on update deltas tracked by the app
    // registry cache.
    {
      ScopedListPrefUpdate apps_installed_pref(
          profile()->GetPrefs(), ::ash::reporting::kAppsInstalled);
      apps_installed_pref->Append(kAppId);
    }

  // Attempt to install the app being tracked above.
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());
  InstallOneApp(kAppId, ::apps::AppType::kStandaloneBrowser,
                /*publisher_id=*/"", ::apps::Readiness::kReady,
                ::apps::InstallSource::kBrowser);

  // Verify that no data is being reported.
  ASSERT_FALSE(test_future.IsReady());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kAppId).Times(1));
    // Since the app is already being tracked, there will be no additional UMA
    // reports from the tracker.
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        1);
}

TEST_F(AppEventsObserverTest, OnAppLaunched) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app launch for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->Launch(kTestAppId, ui::EF_NONE, apps::LaunchSource::kFromCommandLine,
                nullptr);

  // Verify data being reported.
  const MetricData& result = test_future.Take();
  ASSERT_TRUE(result.has_event_data());
  EXPECT_THAT(result.event_data().type(), Eq(MetricEventType::APP_LAUNCHED));
  ASSERT_TRUE(result.has_telemetry_data());
  ASSERT_TRUE(result.telemetry_data().has_app_telemetry());
  ASSERT_TRUE(result.telemetry_data().app_telemetry().has_app_launch_data());

  const AppLaunchData& app_launch_data =
      result.telemetry_data().app_telemetry().app_launch_data();
  EXPECT_THAT(app_launch_data.app_id(), StrEq(kTestAppPublisherId));
  EXPECT_THAT(app_launch_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_ARC));
  EXPECT_THAT(app_launch_data.app_launch_source(),
              Eq(::apps::ApplicationLaunchSource::
                     APPLICATION_LAUNCH_SOURCE_COMMAND_LINE));
}

TEST_F(AppEventsObserverTest, OnAppLaunched_UnsetPolicy) {
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app launch for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->Launch(kTestAppId, ui::EF_NONE, apps::LaunchSource::kFromCommandLine,
                nullptr);

  // Verify no data is being reported.
  ASSERT_FALSE(test_future.IsReady());
}

TEST_F(AppEventsObserverTest, OnAppLaunched_DisallowedAppType) {
  // Set policy to enable reporting for a different app type than the one being
  // tested.
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryGames});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app launch for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->Launch(kTestAppId, ui::EF_NONE, apps::LaunchSource::kFromCommandLine,
                nullptr);

  // Verify no data is being reported.
  ASSERT_FALSE(test_future.IsReady());
}

TEST_F(AppEventsObserverTest, OnAppUninstalled) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});
    ASSERT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kTestAppId).Times(1));

  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app uninstall for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->UninstallSilently(kTestAppId, ::apps::UninstallSource::kAppList);

  // Verify data being reported.
  const MetricData& result = test_future.Take();
  ASSERT_TRUE(result.has_event_data());
  EXPECT_THAT(result.event_data().type(), Eq(MetricEventType::APP_UNINSTALLED));
  ASSERT_TRUE(result.has_telemetry_data());
  ASSERT_TRUE(result.telemetry_data().has_app_telemetry());
  ASSERT_TRUE(result.telemetry_data().app_telemetry().has_app_uninstall_data());

  const AppUninstallData& app_uninstall_data =
      result.telemetry_data().app_telemetry().app_uninstall_data();
  EXPECT_THAT(app_uninstall_data.app_id(), StrEq(kTestAppPublisherId));
  EXPECT_THAT(app_uninstall_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_ARC));
  EXPECT_THAT(app_uninstall_data.app_uninstall_source(),
              Eq(::apps::ApplicationUninstallSource::
                     APPLICATION_UNINSTALL_SOURCE_APP_LIST));

  // Also verify the app is no longer being tracked.
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kTestAppId).Times(0));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppUninstalled_UnsetPolicy) {
  // Verify that the pre-installed app is being tracked by the app install
  // tracker.
  ASSERT_THAT(profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
              Contains(kTestAppId).Times(1));

  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app uninstall for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->UninstallSilently(kTestAppId, ::apps::UninstallSource::kAppList);

  // Verify no data is being reported and the app is no longer being tracked.
  ASSERT_FALSE(test_future.IsReady());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kTestAppId).Times(0));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppUninstalled_DisallowedAppType) {
  // Set policy to enable reporting for a different app type than the one being
  // tested.
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryGames});
    ASSERT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kTestAppId).Times(1));

  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Simulate app uninstall for pre-installed app.
  auto* const proxy = ::apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetAppPlatformMetricsServiceForTesting(GetAppPlatformMetricsService());
  FakePublisher fake_publisher(proxy, ::apps::AppType::kArc);
  proxy->UninstallSilently(kTestAppId, ::apps::UninstallSource::kAppList);

  // Verify no data is being reported and the app is no longer being tracked.
  ASSERT_FALSE(test_future.IsReady());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(kTestAppId).Times(0));
    histogram_tester().ExpectTotalCount(kAppInstallTrackerDiskConsumptionMetric,
                                        2);
}

TEST_F(AppEventsObserverTest, OnAppPlatformMetricsDestroyed) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryBrowser});
  base::test::TestFuture<MetricData> test_future;
  app_events_observer_->SetOnEventObservedCallback(
      test_future.GetRepeatingCallback());

  // Reset `AppPlatformMetricsService` to destroy the `AppPlatformMetrics`
  // component.
  ResetAppPlatformMetricsService();

  // Verify observer is unregistered by attempting to install an app and no
  // metric data being reported.
  static constexpr char app_id[] = "TestNewApp";
  InstallOneApp(app_id, ::apps::AppType::kStandaloneBrowser,
                /*publisher_id=*/"", ::apps::Readiness::kReady,
                ::apps::InstallSource::kBrowser);
  ASSERT_FALSE(test_future.IsReady());
}

}  // namespace
}  // namespace reporting