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

// Copyright 2023 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_usage_observer.h"

#include <memory>
#include <string>
#include <vector>

#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "base/values.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/ash/policy/reporting/metrics_reporting/metric_reporting_prefs.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_types.h"
#include "components/services/app_service/public/cpp/instance.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"
#include "ui/compositor/layer_type.h"

using ::testing::_;
using ::testing::Eq;
using ::testing::NotNull;
using ::testing::StrEq;

namespace reporting {
namespace {

constexpr char kTestAppId[] = "TestApp";
constexpr char kTestAppPublisherId[] = "com.google.test";
constexpr base::TimeDelta kAppUsageCollectionInterval = base::Minutes(5);

// 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 AppUsageObserverTest : public ::apps::AppPlatformMetricsServiceTestBase {
 protected:
  void SetUp() override {
    ::apps::AppPlatformMetricsServiceTestBase::SetUp();

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

    // Pre-install app so it can be used by tests to simulate usage.
    InstallOneApp(kTestAppId, ::apps::AppType::kArc, kTestAppPublisherId,
                  ::apps::Readiness::kReady, ::apps::InstallSource::kPlayStore);

    // Set up `AppUsageObserver` 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_usage_observer_ = AppUsageObserver::CreateForTest(
        profile(), &reporting_settings_,
        std::move(mock_app_platform_metrics_retriever));
  }

  void SimulateAppUsageForInstance(::aura::Window* window,
                                   const base::UnguessableToken& instance_id,
                                   const base::TimeDelta& usage_duration) {
    // Set the window active state and simulate app usage by advancing timer.
    ModifyInstance(instance_id, kTestAppId, window,
                   ::apps::InstanceState::kActive);
    task_environment_.FastForwardBy(usage_duration);

    // Set app inactive by closing window.
    ModifyInstance(instance_id, kTestAppId, window,
                   ::apps::InstanceState::kDestroyed);

    // Advance timer by the collection interval to trigger metric collection.
    task_environment_.FastForwardBy(kAppUsageCollectionInterval);
  }

  void VerifyAppUsageDataInPrefStoreForInstance(
      const base::UnguessableToken& instance_id,
      const base::TimeDelta& expected_usage_time) {
    const auto& usage_dict_pref =
        GetPrefService()->GetDict(::apps::kAppUsageTime);
    ASSERT_THAT(usage_dict_pref.Find(instance_id.ToString()), NotNull());
    EXPECT_THAT(*usage_dict_pref.FindDict(instance_id.ToString())
                     ->FindString(::apps::kUsageTimeAppIdKey),
                StrEq(kTestAppId));
    EXPECT_THAT(*usage_dict_pref.FindDict(instance_id.ToString())
                     ->FindString(::apps::kUsageTimeAppPublisherIdKey),
                StrEq(kTestAppPublisherId));
    EXPECT_THAT(base::ValueToTimeDelta(
                    usage_dict_pref.FindDict(instance_id.ToString())
                        ->Find(::apps::kReportingUsageTimeDurationKey)),
                Eq(expected_usage_time));
  }

  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::kReportAppUsage,
                                std::move(allowed_app_types));
  }

  // Fake reporting settings component used by the test.
  test::FakeReportingSettings reporting_settings_;

  // App usage observer used by tests.
  std::unique_ptr<AppUsageObserver> app_usage_observer_;
};

TEST_F(AppUsageObserverTest, PersistAppUsageDataInPrefStore) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify data is persisted in the pref store.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId, kAppUsageDuration);
}

TEST_F(AppUsageObserverTest, ShouldNotPersistMicrosecondUsageData) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

  // Create a new window for the app and simulate insignificant app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Microseconds(200);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify data is not persisted in the pref store.
  ASSERT_TRUE(GetPrefService()->GetDict(::apps::kAppUsageTime).empty());
}

TEST_F(AppUsageObserverTest, ShouldNotPersistAppUsageDataIfSettingUnset) {
  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify there is no data persisted to the pref store.
  const auto& usage_dict_pref =
      GetPrefService()->GetDict(::apps::kAppUsageTime);
  ASSERT_TRUE(usage_dict_pref.empty());
}

TEST_F(AppUsageObserverTest, ShouldNotPersistAppUsageDataIfAppTypeDisallowed) {
  // Set policy to enable reporting for a different app type than the one being
  // used.
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});

  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify there is no data persisted to the pref store.
  const auto& usage_dict_pref =
      GetPrefService()->GetDict(::apps::kAppUsageTime);
  ASSERT_TRUE(usage_dict_pref.empty());
}

TEST_F(AppUsageObserverTest, ShouldAggregateAppUsageDataOnSubsequentUsage) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify data is persisted to the pref store.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId, kAppUsageDuration);

  // Simulate additional app usage.
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify aggregated usage data is persisted in the pref store.
  const auto& expected_usage_duration = kAppUsageDuration + kAppUsageDuration;
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId,
                                           expected_usage_duration);
}

TEST_F(AppUsageObserverTest,
       ShouldStopPersistingAppUsageDataWhenSettingDisabled) {
  // Allow reporting for specified app type initially.
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify data is persisted to the pref store.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId, kAppUsageDuration);

  // Disallow reporting and simulate additional app usage.
  SetAllowedAppReportingTypes({});
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify usage data remains unchanged in the pref store.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId, kAppUsageDuration);
}

TEST_F(AppUsageObserverTest, ShouldPersistAppUsageDataForNewInstance) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

  // Create a new window for the app and simulate app usage.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify data is persisted to the pref store.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(1UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId, kAppUsageDuration);

  // Simulate app usage for a new instance of the same app.
  ::aura::Window window1(nullptr);
  window1.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId1 = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window1, kInstanceId1, kAppUsageDuration);

  // Verify the component persists usage data for the new instance.
  ASSERT_THAT(GetPrefService()->GetDict(::apps::kAppUsageTime).size(), Eq(2UL));
  VerifyAppUsageDataInPrefStoreForInstance(kInstanceId1, kAppUsageDuration);
}

TEST_F(AppUsageObserverTest, OnAppPlatformMetricsDestroyed) {
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryAndroidApps});

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

  // Create a new window for the app and simulate app usage to verify the
  // component is no longer tracking app usage metric.
  static constexpr base::TimeDelta kAppUsageDuration = base::Minutes(2);
  ::aura::Window window(nullptr);
  window.Init(::ui::LAYER_NOT_DRAWN);
  const base::UnguessableToken& kInstanceId = base::UnguessableToken::Create();
  SimulateAppUsageForInstance(&window, kInstanceId, kAppUsageDuration);

  // Verify there is no data persisted to the pref store.
  const auto& usage_dict_pref =
      GetPrefService()->GetDict(::apps::kAppUsageTime);
  ASSERT_TRUE(usage_dict_pref.empty());
}

}  // namespace
}  // namespace reporting