chromium/chromeos/ash/components/phonehub/recent_apps_interaction_handler_impl_unittest.cc

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

#include "chromeos/ash/components/phonehub/recent_apps_interaction_handler_impl.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/webui/eche_app_ui/eche_connection_status_handler.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/components/phonehub/fake_multidevice_feature_access_manager.h"
#include "chromeos/ash/components/phonehub/notification.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/fake_multidevice_setup_client.h"
#include "chromeos/ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.h"
#include "components/prefs/testing_pref_service.h"
#include "recent_apps_interaction_handler.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"

namespace ash::phonehub {

namespace {

using FeatureState = multidevice_setup::mojom::FeatureState;
using HostStatus = multidevice_setup::mojom::HostStatus;
using ConnectionStatus = eche_app::mojom::ConnectionStatus;

// Garbage color for the purpose of verification in these tests.
const SkColor kIconColor = SkColorSetRGB(0x12, 0x34, 0x56);
const char kIconColorR[] = "icon_color_r";
const char kIconColorG[] = "icon_color_g";
const char kIconColorB[] = "icon_color_b";
const char kIconIsMonochrome[] = "icon_is_monochrome";

class FakeClickHandler : public RecentAppClickObserver {
 public:
  FakeClickHandler() = default;
  ~FakeClickHandler() override = default;

  std::string get_package_name() { return package_name; }

  void OnRecentAppClicked(
      const Notification::AppMetadata& app_metadata,
      eche_app::mojom::AppStreamLaunchEntryPoint entrypoint) override {
    package_name = app_metadata.package_name;
  }

 private:
  std::string package_name;
};

}  // namespace

class RecentAppsInteractionHandlerTest : public testing::Test {
 protected:
  RecentAppsInteractionHandlerTest() = default;
  RecentAppsInteractionHandlerTest(const RecentAppsInteractionHandlerTest&) =
      delete;
  RecentAppsInteractionHandlerTest& operator=(
      const RecentAppsInteractionHandlerTest&) = delete;
  ~RecentAppsInteractionHandlerTest() override = default;

  // testing::Test:
  void SetUp() override {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/{features::kEcheSWA,
                              features::kEcheNetworkConnectionState},
        /*disabled_features=*/{});

    RecentAppsInteractionHandlerImpl::RegisterPrefs(pref_service_.registry());
    fake_multidevice_setup_client_ =
        std::make_unique<multidevice_setup::FakeMultiDeviceSetupClient>();
    interaction_handler_ = std::make_unique<RecentAppsInteractionHandlerImpl>(
        &pref_service_, fake_multidevice_setup_client_.get(),
        &fake_multidevice_feature_access_manager_);
    interaction_handler_->AddRecentAppClickObserver(&fake_click_handler_);

    eche_connection_status_handler_ =
        std::make_unique<eche_app::EcheConnectionStatusHandler>();

    interaction_handler_->SetConnectionStatusHandler(
        eche_connection_status_handler_.get());
  }

  void TearDown() override {
    interaction_handler_->RemoveRecentAppClickObserver(&fake_click_handler_);
  }

  void SaveRecentAppsToPref() {
    const char16_t app_visible_name1[] = u"Fake App";
    const char package_name1[] = "com.fakeapp";
    const int64_t expected_user_id1 = 1;
    auto app_metadata1 = Notification::AppMetadata(
        app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/std::nullopt,
        /*icon_color=*/kIconColor, /*icon_is_monochrome=*/true,
        expected_user_id1, proto::AppStreamabilityStatus::STREAMABLE);

    const char16_t app_visible_name2[] = u"Fake App2";
    const char package_name2[] = "com.fakeapp2";
    const int64_t expected_user_id2 = 2;
    auto app_metadata2 = Notification::AppMetadata(
        app_visible_name2, package_name2, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/gfx::Image(),
        /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/false,
        expected_user_id2, proto::AppStreamabilityStatus::STREAMABLE);

    base::Value::List app_metadata_value_list;
    app_metadata_value_list.Append(app_metadata1.ToValue());
    app_metadata_value_list.Append(app_metadata2.ToValue());

    pref_service_.SetList(prefs::kRecentAppsHistory,
                          std::move(app_metadata_value_list));
  }

  void SaveLegacyRecentAppToPref() {
    const char16_t app_visible_name1[] = u"Fake App";
    const char package_name1[] = "com.fakeapp";
    const int64_t expected_user_id1 = 1;
    base::Value::Dict app_metadata_value =
        Notification::AppMetadata(
            app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
            /*monochrome_icon_mask=*/std::nullopt,
            /*icon_color=*/kIconColor, /*icon_is_monochrome=*/false,
            expected_user_id1, proto::AppStreamabilityStatus::STREAMABLE)
            .ToValue();

    // Simulate an un-migrated preference without new fields.
    EXPECT_TRUE(app_metadata_value.Remove(kIconIsMonochrome));
    EXPECT_TRUE(app_metadata_value.Remove(kIconColorR));
    EXPECT_TRUE(app_metadata_value.Remove(kIconColorG));
    EXPECT_TRUE(app_metadata_value.Remove(kIconColorB));

    base::Value::List app_metadata_value_list;
    app_metadata_value_list.Append(std::move(app_metadata_value));

    pref_service_.SetList(prefs::kRecentAppsHistory,
                          std::move(app_metadata_value_list));
  }

  std::string GetPackageName() {
    return fake_click_handler_.get_package_name();
  }

  RecentAppsInteractionHandlerImpl& handler() { return *interaction_handler_; }

  void SetEcheFeatureState(FeatureState feature_state) {
    fake_multidevice_setup_client_->SetFeatureState(
        multidevice_setup::mojom::Feature::kEche, feature_state);
  }

  void SetPhoneHubNotificationsFeatureState(FeatureState feature_state) {
    fake_multidevice_setup_client_->SetFeatureState(
        multidevice_setup::mojom::Feature::kPhoneHubNotifications,
        feature_state);
  }

  void SetHostStatus(HostStatus host_status) {
    fake_multidevice_setup_client_->SetHostStatusWithDevice(
        std::make_pair(host_status, std::nullopt /* host_device */));
  }

  void SetNotificationAccess(bool enabled) {
    fake_multidevice_feature_access_manager_
        .SetNotificationAccessStatusInternal(
            enabled
                ? MultideviceFeatureAccessManager::AccessStatus::kAccessGranted
                : MultideviceFeatureAccessManager::AccessStatus::
                      kAvailableButNotGranted,
            MultideviceFeatureAccessManager::AccessProhibitedReason::kUnknown);
  }

  void SetAppsAccessStatus(bool enabled) {
    fake_multidevice_feature_access_manager_.SetAppsAccessStatusInternal(
        enabled ? MultideviceFeatureAccessManager::AccessStatus::kAccessGranted
                : MultideviceFeatureAccessManager::AccessStatus::
                      kAvailableButNotGranted);
  }

  std::vector<RecentAppsInteractionHandler::UserState> GetDefaultUserStates() {
    RecentAppsInteractionHandler::UserState user_state1;
    user_state1.user_id = 1;
    user_state1.is_enabled = true;
    RecentAppsInteractionHandler::UserState user_state2;
    user_state2.user_id = 2;
    user_state2.is_enabled = true;
    std::vector<RecentAppsInteractionHandler::UserState> user_states;
    user_states.push_back(user_state1);
    user_states.push_back(user_state2);
    return user_states;
  }

  std::vector<RecentAppsInteractionHandler::UserState>
  GetWorkProfileTurnedOffUserStates() {
    RecentAppsInteractionHandler::UserState user_state1;
    user_state1.user_id = 1;
    user_state1.is_enabled = true;
    RecentAppsInteractionHandler::UserState user_state2;
    user_state2.user_id = 2;
    user_state2.is_enabled = false;
    std::vector<RecentAppsInteractionHandler::UserState> user_states;
    user_states.push_back(user_state1);
    user_states.push_back(user_state2);
    return user_states;
  }

  void GenerateDefaultAppMetadata() {
    const base::Time now = base::Time::Now();
    const char16_t app_visible_name1[] = u"Fake App1";
    const char package_name1[] = "com.fakeapp1";
    const int64_t expected_user_id1 = 1;
    auto app_metadata1 = Notification::AppMetadata(
        app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/std::nullopt,
        /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
        expected_user_id1, proto::AppStreamabilityStatus::STREAMABLE);
    const char16_t app_visible_name2[] = u"Fake App2";
    const char package_name2[] = "com.fakeapp2";
    const int64_t expected_user_id2 = 2;
    auto app_metadata2 = Notification::AppMetadata(
        app_visible_name2, package_name2, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/gfx::Image(),
        /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
        expected_user_id2, proto::AppStreamabilityStatus::STREAMABLE);
    handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
    handler().NotifyRecentAppAddedOrUpdated(app_metadata2, now);
  }

  void GenerateAppMetadataWithDuplicateUserId() {
    const base::Time now = base::Time::Now();
    const char16_t app_visible_name1[] = u"Fake App1";
    const char package_name1[] = "com.fakeapp1";
    const int64_t expected_user_id = 1;
    auto app_metadata1 = Notification::AppMetadata(
        app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/std::nullopt,
        /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
        expected_user_id, proto::AppStreamabilityStatus::STREAMABLE);
    const char16_t app_visible_name2[] = u"Fake App2";
    const char package_name2[] = "com.fakeapp2";
    auto app_metadata2 = Notification::AppMetadata(
        app_visible_name2, package_name2, /*color_icon=*/gfx::Image(),
        /*monochrome_icon_mask=*/gfx::Image(),
        /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
        expected_user_id, proto::AppStreamabilityStatus::STREAMABLE);
    handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
    handler().NotifyRecentAppAddedOrUpdated(app_metadata2, now);
  }

  void NotifyConnectionStatusChanged(ConnectionStatus connection_status) {
    eche_connection_status_handler_->OnConnectionStatusChanged(
        connection_status);
  }

  void SetConnectionStatus(ConnectionStatus connection_status) {
    interaction_handler_->set_connection_status_for_testing(connection_status);
  }

  RecentAppsInteractionHandler::RecentAppsUiState
  GetUiStateFromConnectionStatus() {
    return interaction_handler_->GetUiStateFromConnectionStatus();
  }

  std::unique_ptr<multidevice_setup::FakeMultiDeviceSetupClient>
      fake_multidevice_setup_client_;
  base::test::ScopedFeatureList feature_list_;

 private:
  FakeClickHandler fake_click_handler_;
  std::unique_ptr<eche_app::EcheConnectionStatusHandler>
      eche_connection_status_handler_;
  TestingPrefServiceSimple pref_service_;
  FakeMultideviceFeatureAccessManager fake_multidevice_feature_access_manager_;
  std::unique_ptr<RecentAppsInteractionHandlerImpl> interaction_handler_;
};

TEST_F(RecentAppsInteractionHandlerTest, RecentAppsClicked) {
  const char16_t expected_app_visible_name[] = u"Fake App";
  const char expected_package_name[] = "com.fakeapp";
  const int64_t expected_user_id = 1;
  auto expected_app_metadata = Notification::AppMetadata(
      expected_app_visible_name, expected_package_name,
      /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
      expected_user_id, proto::AppStreamabilityStatus::STREAMABLE);

  handler().NotifyRecentAppClicked(
      expected_app_metadata,
      eche_app::mojom::AppStreamLaunchEntryPoint::RECENT_APPS);

  EXPECT_EQ(expected_package_name, GetPackageName());
}

TEST_F(RecentAppsInteractionHandlerTest, RecentAppsClickedHasOriginalIcon) {
  const char16_t expected_app_visible_name[] = u"Fake App";
  const char expected_package_name[] = "com.fakeapp";
  const int64_t expected_user_id = 1;
  auto expected_app_metadata = Notification::AppMetadata(
      expected_app_visible_name, expected_package_name,
      /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/gfx::Image(),
      /*icon_color=*/std::nullopt, /*icon_is_monochrome=*/true,
      expected_user_id, proto::AppStreamabilityStatus::STREAMABLE);

  handler().NotifyRecentAppClicked(
      expected_app_metadata,
      eche_app::mojom::AppStreamLaunchEntryPoint::RECENT_APPS);

  EXPECT_EQ(expected_package_name, GetPackageName());
}

TEST_F(RecentAppsInteractionHandlerTest, RecentAppsUpdated) {
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);

  const char16_t app_visible_name2[] = u"Fake App2";
  const char package_name2[] = "com.fakeapp2";
  const int64_t expected_user_id2 = 2;
  auto app_metadata2 = Notification::AppMetadata(
      app_visible_name2, package_name2, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/gfx::Image(),
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id2,
      proto::AppStreamabilityStatus::STREAMABLE);
  const base::Time now = base::Time::Now();

  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
  EXPECT_EQ(1U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ(now,
            handler().recent_app_metadata_list_for_testing()->at(0).second);

  // The same package name only update last accessed timestamp.
  const base::Time next_minute = base::Time::Now() + base::Minutes(1);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, next_minute);
  EXPECT_EQ(1U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ(next_minute,
            handler().recent_app_metadata_list_for_testing()->at(0).second);

  const base::Time next_hour = base::Time::Now() + base::Hours(1);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata2, next_hour);
  EXPECT_EQ(2U, handler().recent_app_metadata_list_for_testing()->size());
}

TEST_F(RecentAppsInteractionHandlerTest, SetStreamableApps) {
  std::vector<Notification::AppMetadata> streamable_apps;
  streamable_apps.emplace_back(u"App1", "com.fakeapp1",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/std::nullopt,
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);
  streamable_apps.emplace_back(u"App2", "com.fakeapp2",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/gfx::Image(),
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);

  handler().SetStreamableApps(streamable_apps);

  EXPECT_EQ(2U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ("com.fakeapp1", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(0)
                                .first.package_name);
  EXPECT_EQ("com.fakeapp2", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(1)
                                .first.package_name);
}

TEST_F(RecentAppsInteractionHandlerTest,
       SetStreamableApps_ClearsPreviousState) {
  std::vector<Notification::AppMetadata> streamable_apps;
  streamable_apps.emplace_back(u"App1", "com.fakeapp1",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/std::nullopt,
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);
  streamable_apps.emplace_back(u"App2", "com.fakeapp2",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/gfx::Image(),
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);

  handler().SetStreamableApps(streamable_apps);

  EXPECT_EQ(2U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ("com.fakeapp1", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(0)
                                .first.package_name);
  EXPECT_EQ("com.fakeapp2", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(1)
                                .first.package_name);

  std::vector<Notification::AppMetadata> streamable_apps2;
  streamable_apps2.emplace_back(u"App3", "com.fakeapp3",
                                /*color_icon=*/gfx::Image(),
                                /*monochrome_icon_mask=*/std::nullopt,
                                /*icon_color=*/std::nullopt,
                                /*icon_is_monochrome=*/true, 1,
                                proto::AppStreamabilityStatus::STREAMABLE);

  handler().SetStreamableApps(streamable_apps2);

  EXPECT_EQ(1U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ("com.fakeapp3", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(0)
                                .first.package_name);
}

TEST_F(RecentAppsInteractionHandlerTest, SetStreamableApps_EmptyList) {
  std::vector<Notification::AppMetadata> streamable_apps;

  handler().SetStreamableApps(streamable_apps);

  EXPECT_TRUE(handler().recent_app_metadata_list_for_testing()->empty());
}

TEST_F(RecentAppsInteractionHandlerTest, RemoveStreamableApp) {
  std::vector<Notification::AppMetadata> streamable_apps;
  streamable_apps.emplace_back(u"App1", "com.fakeapp1",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/std::nullopt,
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);
  streamable_apps.emplace_back(u"App2", "com.fakeapp2",
                               /*color_icon=*/gfx::Image(),
                               /*monochrome_icon_mask=*/gfx::Image(),
                               /*icon_color=*/std::nullopt,
                               /*icon_is_monochrome=*/true, 1,
                               proto::AppStreamabilityStatus::STREAMABLE);

  handler().SetStreamableApps(streamable_apps);

  EXPECT_EQ(2U, handler().recent_app_metadata_list_for_testing()->size());

  auto app_to_remove = proto::App();
  app_to_remove.set_package_name("com.fakeapp1");
  app_to_remove.set_visible_name("App1");
  handler().RemoveStreamableApp(app_to_remove);
  EXPECT_EQ(1U, handler().recent_app_metadata_list_for_testing()->size());
  EXPECT_EQ("com.fakeapp2", handler()
                                .recent_app_metadata_list_for_testing()
                                ->at(0)
                                .first.package_name);
}

TEST_F(RecentAppsInteractionHandlerTest, FetchRecentAppMetadataList) {
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);

  const char16_t app_visible_name2[] = u"Fake App2";
  const char package_name2[] = "com.fakeapp2";
  const int64_t expected_user_id2 = 1;
  auto app_metadata2 = Notification::AppMetadata(
      app_visible_name2, package_name2, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/gfx::Image(),
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id2,
      proto::AppStreamabilityStatus::STREAMABLE);

  const char16_t app_visible_name3[] = u"Fake App3";
  const char package_name3[] = "com.fakeapp3";
  const int64_t expected_user_id3 = 1;
  auto app_metadata3 = Notification::AppMetadata(
      app_visible_name3, package_name3, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id3,
      proto::AppStreamabilityStatus::STREAMABLE);

  const base::Time now = base::Time::Now();
  const base::Time next_minute = base::Time::Now() + base::Minutes(1);
  const base::Time next_hour = base::Time::Now() + base::Hours(1);

  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata2, next_minute);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata3, next_hour);

  std::vector<RecentAppsInteractionHandler::UserState> user_states =
      GetDefaultUserStates();
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(3U, recent_apps_metadata_result.size());
  EXPECT_EQ(package_name3, recent_apps_metadata_result[0].package_name);
  EXPECT_EQ(package_name2, recent_apps_metadata_result[1].package_name);
  EXPECT_EQ(package_name1, recent_apps_metadata_result[2].package_name);

  const char16_t app_visible_name4[] = u"Fake App4";
  const char package_name4[] = "com.fakeapp4";
  const int64_t expected_user_id4 = 1;
  auto app_metadata4 = Notification::AppMetadata(
      app_visible_name4, package_name4, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id4,
      proto::AppStreamabilityStatus::STREAMABLE);

  const char16_t app_visible_name5[] = u"Fake App5";
  const char package_name5[] = "com.fakeapp5";
  const int64_t expected_user_id5 = 1;
  auto app_metadata5 = Notification::AppMetadata(
      app_visible_name5, package_name5, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id5,
      proto::AppStreamabilityStatus::STREAMABLE);

  const base::Time next_two_hour = base::Time::Now() + base::Hours(2);
  const base::Time next_three_hour = base::Time::Now() + base::Hours(3);

  handler().NotifyRecentAppAddedOrUpdated(app_metadata4, next_two_hour);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata5, next_three_hour);

  const size_t max_most_recent_apps = 5;
  recent_apps_metadata_result = handler().FetchRecentAppMetadataList();
  EXPECT_EQ(max_most_recent_apps, recent_apps_metadata_result.size());

  EXPECT_EQ(package_name5, recent_apps_metadata_result[0].package_name);
  EXPECT_EQ(package_name4, recent_apps_metadata_result[1].package_name);
  EXPECT_EQ(package_name3, recent_apps_metadata_result[2].package_name);
  EXPECT_EQ(package_name2, recent_apps_metadata_result[3].package_name);
  EXPECT_EQ(package_name1, recent_apps_metadata_result[4].package_name);
}

TEST_F(RecentAppsInteractionHandlerTest,
       FetchRecentAppMetadataListFromPreference) {
  SaveRecentAppsToPref();

  std::vector<RecentAppsInteractionHandler::UserState> user_states =
      GetDefaultUserStates();
  handler().set_user_states(user_states);

  const char package_name1[] = "com.fakeapp";
  const char package_name2[] = "com.fakeapp2";
  const size_t number_of_recent_apps_in_preference = 2;

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(number_of_recent_apps_in_preference,
            recent_apps_metadata_result.size());
  EXPECT_EQ(package_name1, recent_apps_metadata_result[0].package_name);
  EXPECT_EQ(package_name2, recent_apps_metadata_result[1].package_name);

  // Check de/serialization of icon metadata
  EXPECT_TRUE(recent_apps_metadata_result[0].icon_color.has_value());
  EXPECT_TRUE(*recent_apps_metadata_result[0].icon_color == kIconColor);
  EXPECT_TRUE(recent_apps_metadata_result[0].icon_is_monochrome);
  EXPECT_FALSE(recent_apps_metadata_result[1].icon_color.has_value());
  EXPECT_FALSE(recent_apps_metadata_result[1].icon_is_monochrome);
}

TEST_F(RecentAppsInteractionHandlerTest,
       FetchRecentAppMetadataListFromPreferenceBackwardsCompat) {
  SaveLegacyRecentAppToPref();

  std::vector<RecentAppsInteractionHandler::UserState> user_states =
      GetDefaultUserStates();
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();
  EXPECT_EQ(1u, recent_apps_metadata_result.size());

  // Check that new fields are appropriately filled in with safe defaults.
  EXPECT_FALSE(recent_apps_metadata_result[0].icon_color.has_value());
  EXPECT_FALSE(recent_apps_metadata_result[0].icon_is_monochrome);
}

TEST_F(RecentAppsInteractionHandlerTest,
       OnFeatureStatesChangedToDisabledWithEmptyRecentAppsList) {
  SetEcheFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       OnFeatureStatesChangedToDisabledWithNonEmptyRecentAppsList) {
  const base::Time now = base::Time::Now();
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);

  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
  SetEcheFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       OnFeatureStatesChangedToEnabledWithEmptyRecentAppsList) {
  SetEcheFeatureState(FeatureState::kEnabledByUser);
  SetPhoneHubNotificationsFeatureState(FeatureState::kEnabledByUser);
  SetAppsAccessStatus(true);
  SetNotificationAccess(true);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::LOADING,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       DisableNotificationAccessWithEmptyRecentAppsList) {
  SetEcheFeatureState(FeatureState::kEnabledByUser);
  SetPhoneHubNotificationsFeatureState(FeatureState::kEnabledByUser);
  SetAppsAccessStatus(true);
  SetNotificationAccess(true);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::LOADING,
            handler().ui_state());

  // Disable apps access permission on the host device.
  SetAppsAccessStatus(false);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Disable apps access permission on the local device.
  SetAppsAccessStatus(true);
  SetEcheFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Disable apps access permission on both devices.
  SetAppsAccessStatus(false);
  SetEcheFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Enable apps access permission back on both devices.
  SetAppsAccessStatus(true);
  SetEcheFeatureState(FeatureState::kEnabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::LOADING,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       OnFeatureStatesChangedToEnabledWithNonEmptyRecentAppsList) {
  const base::Time now = base::Time::Now();
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);
  SetConnectionStatus(ConnectionStatus::kConnectionStatusConnected);
  SetAppsAccessStatus(true);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
  SetEcheFeatureState(FeatureState::kEnabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       DisableNotificationAccessWithNonEmptyRecentAppsList) {
  const base::Time now = base::Time::Now();
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);

  SetConnectionStatus(ConnectionStatus::kConnectionStatusConnected);
  SetAppsAccessStatus(true);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);
  SetEcheFeatureState(FeatureState::kEnabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());

  // Disable notification access permission on the host device.
  SetNotificationAccess(false);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());

  // Disable notification access permission on the local device.
  SetNotificationAccess(true);
  SetPhoneHubNotificationsFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());

  // Disable notification access permission on both devices.
  SetNotificationAccess(false);
  SetPhoneHubNotificationsFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       UiStateChangedToVisibleWhenRecentAppBeAdded) {
  feature_list_.Reset();
  feature_list_.InitWithFeatures(
      /*enabled_features=*/{features::kEcheSWA},
      /*disabled_features=*/{features::kEcheNetworkConnectionState});

  SetEcheFeatureState(FeatureState::kEnabledByUser);
  SetPhoneHubNotificationsFeatureState(FeatureState::kEnabledByUser);
  SetConnectionStatus(ConnectionStatus::kConnectionStatusConnected);
  SetAppsAccessStatus(true);
  SetNotificationAccess(true);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::PLACEHOLDER_VIEW,
            handler().ui_state());

  const base::Time now = base::Time::Now();
  const char16_t app_visible_name1[] = u"Fake App";
  const char package_name1[] = "com.fakeapp";
  const int64_t expected_user_id1 = 1;
  auto app_metadata1 = Notification::AppMetadata(
      app_visible_name1, package_name1, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id1,
      proto::AppStreamabilityStatus::STREAMABLE);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata1, now);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest, DisableAppsAccess) {
  GenerateDefaultAppMetadata();

  // The apps access has not been granted yet so the UI state always HIDDEN.
  SetAppsAccessStatus(true);

  SetPhoneHubNotificationsFeatureState(FeatureState::kEnabledByUser);
  SetNotificationAccess(true);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  SetNotificationAccess(false);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Disable notification access permission on the local device.
  SetNotificationAccess(true);
  SetPhoneHubNotificationsFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Disable notification access permission on both devices.
  SetNotificationAccess(false);
  SetPhoneHubNotificationsFeatureState(FeatureState::kDisabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());

  // Enable notification access permission back on both devices.
  SetNotificationAccess(true);
  SetPhoneHubNotificationsFeatureState(FeatureState::kEnabledByUser);

  EXPECT_EQ(RecentAppsInteractionHandler::RecentAppsUiState::HIDDEN,
            handler().ui_state());
}

TEST_F(RecentAppsInteractionHandlerTest,
       PrefBeClearedWhenFeatureStatesChangedToUnavailableNoVerifiedHost) {
  SaveRecentAppsToPref();
  SetHostStatus(HostStatus::kHostSetButNotYetVerified);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 0u);
}

TEST_F(
    RecentAppsInteractionHandlerTest,
    RecentAppsListBeClearedWhenFeatureStatesChangedToUnavailableNoVerifiedHost) {
  const base::Time now = base::Time::Now();
  const char16_t app_visible_name[] = u"Fake App";
  const char package_name[] = "com.fakeapp";
  const int64_t expected_user_id = 1;
  auto app_metadata = Notification::AppMetadata(
      app_visible_name, package_name, /*color_icon=*/gfx::Image(),
      /*monochrome_icon_mask=*/std::nullopt,
      /*icon_color=*/std::nullopt,
      /*icon_is_monochrome=*/true, expected_user_id,
      proto::AppStreamabilityStatus::STREAMABLE);
  handler().NotifyRecentAppAddedOrUpdated(app_metadata, now);
  SetHostStatus(HostStatus::kHostSetButNotYetVerified);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 0u);
}

TEST_F(RecentAppsInteractionHandlerTest,
       ShowAllRecentAppsOfAllUsersWithQuietModeOff) {
  GenerateDefaultAppMetadata();

  std::vector<RecentAppsInteractionHandler::UserState> user_states =
      GetDefaultUserStates();
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 2u);
  EXPECT_EQ(2, recent_apps_metadata_result[0].user_id);
  EXPECT_EQ("com.fakeapp2", recent_apps_metadata_result[0].package_name);
  EXPECT_EQ(1, recent_apps_metadata_result[1].user_id);
  EXPECT_EQ("com.fakeapp1", recent_apps_metadata_result[1].package_name);
}

TEST_F(RecentAppsInteractionHandlerTest, ShowRecentAppsOfUserWithQuietModeOn) {
  GenerateDefaultAppMetadata();

  std::vector<RecentAppsInteractionHandler::UserState> user_states =
      GetWorkProfileTurnedOffUserStates();
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 1u);
  EXPECT_EQ(1, recent_apps_metadata_result[0].user_id);
  EXPECT_EQ("com.fakeapp1", recent_apps_metadata_result[0].package_name);
}

TEST_F(RecentAppsInteractionHandlerTest, ShowRecentAppsWhenGetsEmptyUser) {
  GenerateDefaultAppMetadata();

  std::vector<RecentAppsInteractionHandler::UserState> user_states;
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 2u);
  // LIFO scheme is used, hence app 2 is first as it was pushed last.
  EXPECT_EQ(2, recent_apps_metadata_result[0].user_id);
  EXPECT_EQ(1, recent_apps_metadata_result[1].user_id);
}

TEST_F(RecentAppsInteractionHandlerTest, GetUserIdSet) {
  GenerateAppMetadataWithDuplicateUserId();

  std::vector<RecentAppsInteractionHandler::UserState> user_states;
  handler().set_user_states(user_states);

  std::vector<Notification::AppMetadata> recent_apps_metadata_result =
      handler().FetchRecentAppMetadataList();

  EXPECT_EQ(recent_apps_metadata_result.size(), 2u);
  EXPECT_EQ(1, recent_apps_metadata_result[0].user_id);
  EXPECT_EQ(1, recent_apps_metadata_result[1].user_id);
}

TEST_F(RecentAppsInteractionHandlerTest, OnConnectionStatusChanged) {
  // Start in the Disconnected state.
  // Handler will only change connection state for Connected and Failed.
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusConnecting);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  // Any transition to "connected" should update ConnUX to connected.
  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusConnected);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusConnected);

  // Only background failures update the ConnUX state, app stream failures
  // should not.
  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusFailed);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusConnected);
  NotifyConnectionStatusChanged(
      ConnectionStatus::kConnectionStatusDisconnected);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusConnected);
}

TEST_F(RecentAppsInteractionHandlerTest,
       OnConnectionStatusChangedFlagDisabled) {
  feature_list_.Reset();
  feature_list_.InitWithFeatures(
      /*enabled_features=*/{features::kEcheSWA},
      /*disabled_features=*/{features::kEcheNetworkConnectionState});

  // Start in the Disconnected state. When flag is disabled, the state should
  // never change.
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusConnecting);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusConnected);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  NotifyConnectionStatusChanged(ConnectionStatus::kConnectionStatusFailed);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);

  NotifyConnectionStatusChanged(
      ConnectionStatus::kConnectionStatusDisconnected);
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);
}

TEST_F(RecentAppsInteractionHandlerTest, GetUiStateFromConnectionStatus) {
  RecentAppsInteractionHandler::RecentAppsUiState ui_state;

  SetConnectionStatus(ConnectionStatus::kConnectionStatusDisconnected);
  ui_state = GetUiStateFromConnectionStatus();
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);
  EXPECT_EQ(ui_state, RecentAppsInteractionHandler::RecentAppsUiState::LOADING);

  SetConnectionStatus(ConnectionStatus::kConnectionStatusConnecting);
  ui_state = GetUiStateFromConnectionStatus();
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusConnecting);
  EXPECT_EQ(ui_state, RecentAppsInteractionHandler::RecentAppsUiState::LOADING);

  SetConnectionStatus(ConnectionStatus::kConnectionStatusConnected);
  ui_state = GetUiStateFromConnectionStatus();
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusConnected);
  EXPECT_EQ(ui_state,
            RecentAppsInteractionHandler::RecentAppsUiState::ITEMS_VISIBLE);

  SetConnectionStatus(ConnectionStatus::kConnectionStatusFailed);
  ui_state = GetUiStateFromConnectionStatus();
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusFailed);
  EXPECT_EQ(ui_state,
            RecentAppsInteractionHandler::RecentAppsUiState::CONNECTION_FAILED);

  SetConnectionStatus(ConnectionStatus::kConnectionStatusDisconnected);
  ui_state = GetUiStateFromConnectionStatus();
  EXPECT_EQ(handler().connection_status_for_testing(),
            ConnectionStatus::kConnectionStatusDisconnected);
  EXPECT_EQ(ui_state, RecentAppsInteractionHandler::RecentAppsUiState::LOADING);
}

}  // namespace ash::phonehub