// 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 "ash/system/phonehub/onboarding_nudge_controller.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/system/phonehub/phone_hub_tray.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/test/ash_test_base.h"
#include "base/functional/callback_helpers.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/remote_device_test_util.h"
#include "chromeos/ash/components/phonehub/fake_phone_hub_manager.h"
#include "chromeos/ash/components/phonehub/feature_status.h"
#include "chromeos/ash/components/phonehub/feature_status_provider.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "components/session_manager/session_manager_types.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
const char kPhoneBluetoothAddress[] = "23:45:67:89:AB:CD";
constexpr auto kTestTime = base::Time::FromSecondsSinceUnixEpoch(100000);
} // namespace
class OnboardingNudgeControllerTest : public AshTestBase {
public:
OnboardingNudgeControllerTest() = default;
OnboardingNudgeControllerTest(const OnboardingNudgeControllerTest&) = delete;
OnboardingNudgeControllerTest& operator=(
const OnboardingNudgeControllerTest&) = delete;
~OnboardingNudgeControllerTest() override = default;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
test_clock_ = std::make_unique<base::SimpleTestClock>();
widget_ = CreateFramelessTestWidget();
test_clock_->SetNow(kTestTime);
controller_ = std::make_unique<OnboardingNudgeController>(
/*phone_hub_tray=*/widget_->SetContentsView(
std::make_unique<views::View>()),
/*stop_animation_callback=*/base::DoNothing(),
/*start_animation_callback=*/base::DoNothing(), test_clock_.get());
}
protected:
OnboardingNudgeController* GetController() { return controller_.get(); }
multidevice::RemoteDeviceRef CreatePhoneDeviceWithUniqueInstanceId(
bool supports_better_together_host,
bool supports_phone_hub_host,
bool has_bluetooth_address,
std::string instance_id) {
multidevice::RemoteDeviceRefBuilder builder;
builder.SetSoftwareFeatureState(
multidevice::SoftwareFeature::kBetterTogetherHost,
supports_better_together_host
? multidevice::SoftwareFeatureState::kSupported
: multidevice::SoftwareFeatureState::kNotSupported);
builder.SetSoftwareFeatureState(
multidevice::SoftwareFeature::kPhoneHubHost,
supports_phone_hub_host
? multidevice::SoftwareFeatureState::kSupported
: multidevice::SoftwareFeatureState::kNotSupported);
builder.SetBluetoothPublicAddress(
has_bluetooth_address ? kPhoneBluetoothAddress : std::string());
builder.SetInstanceId(instance_id);
return builder.Build();
}
void AdvanceClock(base::TimeDelta delta) { test_clock_->Advance(delta); }
PrefService* pref_service() {
return Shell::Get()->session_controller()->GetActivePrefService();
}
base::test::ScopedFeatureList feature_list_;
base::HistogramTester histogram_tester_;
private:
std::unique_ptr<base::SimpleTestClock> test_clock_;
std::unique_ptr<views::Widget> widget_;
std::unique_ptr<OnboardingNudgeController> controller_;
};
TEST_F(OnboardingNudgeControllerTest, OnboardingNudgeControllerExists) {
OnboardingNudgeController* controller = GetController();
ASSERT_TRUE(controller);
}
TEST_F(OnboardingNudgeControllerTest, ShowNudge) {
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime);
histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 1);
// Advance the clock by 5 minutes, should not show nuge again.
AdvanceClock(base::Minutes(5));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime);
// Advance the clock by 24 hours, should show nuge again.
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
2);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime + base::Minutes(5) + base::Hours(24));
histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 2);
// Advance the clock by 24 hours, should show nuge again.
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
3);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime + base::Minutes(5) + base::Hours(24) + base::Hours(24));
histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 3);
// Should not show nudge again since the total appearances reach 3 times.
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
3);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime + base::Minutes(5) + base::Hours(24) + base::Hours(24));
}
TEST_F(OnboardingNudgeControllerTest, HoverNudge) {
GetController()->OnNudgeHoverStateChanged(false);
EXPECT_TRUE(
pref_service()
->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastActionTime)
.is_null());
GetController()->OnNudgeHoverStateChanged(true);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
kTestTime);
}
TEST_F(OnboardingNudgeControllerTest, ClickNudgeAndPhoneHubIcon) {
GetController()->ShowNudgeIfNeeded();
AdvanceClock(base::Seconds(3));
GetController()->OnNudgeClicked();
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
kTestTime + base::Seconds(3));
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastClickTime),
kTestTime + base::Seconds(3));
// Simulate click on Phone Hub icon.
GetController()->HideNudge();
histogram_tester_.ExpectTimeBucketCount(
"MultiDeviceSetup.NudgeActionDuration", base::Seconds(3), 1);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeShownTimesBeforeActed", 1, 1);
histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeInteracted", 2);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeInteracted",
phone_hub_metrics::MultideviceSetupNudgeInteraction::kNudgeClicked, 1);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeInteracted",
phone_hub_metrics::MultideviceSetupNudgeInteraction::kPhoneHubIconClicked,
1);
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
// Nudge has been clicked, would not show again.
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
kTestTime);
}
TEST_F(OnboardingNudgeControllerTest, ClickPhoneHubIcon) {
GetController()->ShowNudgeIfNeeded();
AdvanceClock(base::Seconds(3));
// Simulate Phone Hub icon clicked.
GetController()->HideNudge();
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
kTestTime + base::Seconds(3));
EXPECT_EQ(pref_service()->GetTime(
OnboardingNudgeController::kPhoneHubNudgeLastClickTime),
kTestTime + base::Seconds(3));
histogram_tester_.ExpectTimeBucketCount(
"MultiDeviceSetup.NudgeActionDuration", base::Seconds(3), 1);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeShownTimesBeforeActed", 1, 1);
histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeInteracted", 1);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeInteracted",
phone_hub_metrics::MultideviceSetupNudgeInteraction::kNudgeClicked, 0);
histogram_tester_.ExpectBucketCount(
"MultiDeviceSetup.NudgeInteracted",
phone_hub_metrics::MultideviceSetupNudgeInteraction::kPhoneHubIconClicked,
1);
}
TEST_F(OnboardingNudgeControllerTest,
AddToSyncedDeviceListWhenNewEligibleDeviceFound) {
multidevice::RemoteDeviceRef device_1 = CreatePhoneDeviceWithUniqueInstanceId(
/*supports_better_together_host=*/true,
/*supports_phone_hub_host=*/true,
/*has_bluetooth_address=*/true, "AAA");
GetController()->OnEligiblePhoneHubHostFound({device_1});
EXPECT_EQ(
pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
1u);
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
2);
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
3);
AdvanceClock(base::Hours(24));
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
3);
// Create eligible second device.
multidevice::RemoteDeviceRef device_2 = CreatePhoneDeviceWithUniqueInstanceId(
/*supports_better_together_host=*/true,
/*supports_phone_hub_host=*/true,
/*has_bluetooth_address=*/true, "AAB");
GetController()->OnEligiblePhoneHubHostFound({device_1, device_2});
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
0);
EXPECT_TRUE(
pref_service()
->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastShownTime)
.is_null());
EXPECT_TRUE(
pref_service()
->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastActionTime)
.is_null());
EXPECT_TRUE(
pref_service()
->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastClickTime)
.is_null());
EXPECT_EQ(
pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
2u);
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
}
TEST_F(OnboardingNudgeControllerTest,
DoNotAddToSyncedDeviceListIfAlreadyFound) {
multidevice::RemoteDeviceRef device_1 = CreatePhoneDeviceWithUniqueInstanceId(
/*supports_better_together_host=*/true,
/*supports_phone_hub_host=*/true,
/*has_bluetooth_address=*/true, "AAA");
GetController()->OnEligiblePhoneHubHostFound({device_1});
EXPECT_EQ(
pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
1u);
GetController()->ShowNudgeIfNeeded();
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
// Synced device list should remain the same size.
GetController()->OnEligiblePhoneHubHostFound({device_1});
// Pref value is not reset if host list remains unchanged.
EXPECT_EQ(pref_service()->GetInteger(
OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
1);
EXPECT_EQ(
pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
1u);
}
} // namespace ash