0

[CrOS Hotspot] Add hotspot detailed view and implementation

This CL adds the hotspot detailed view interface and its implementation.

Screenshots:
https://screenshot.googleplex.com/Bpakc38qJRRA7Be.png
https://screenshot.googleplex.com/9UyWxSXrMPjPYZZ.png
https://screenshot.googleplex.com/9L9he4ALMn7kQGa.png
https://screenshot.googleplex.com/8eJviJswMD24tat.png
https://screenshot.googleplex.com/TBAdu3HFJq5xGWU.png
https://screenshot.googleplex.com/8fwSjKcXxzEPCrR.png
https://screenshot.googleplex.com/7vHkArkXrc5SFQk.png

Bug: b/269353814
Change-Id: Ied8cadefb1b7fc176f1fdade956e56a74a27552a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4522254
Reviewed-by: James Cook <jamescook@chromium.org>
Commit-Queue: Jason Zhang <jiajunz@google.com>
Cr-Commit-Position: refs/heads/main@{#1145701}
This commit is contained in:
Jason Zhang
2023-05-17 23:38:05 +00:00
committed by Chromium LUCI CQ
parent 7286243f09
commit 298498c949
17 changed files with 720 additions and 0 deletions

@ -1407,6 +1407,8 @@ component("ash") {
"system/holding_space/screen_captures_section.h",
"system/holding_space/suggestions_section.cc",
"system/holding_space/suggestions_section.h",
"system/hotspot/hotspot_detailed_view.cc",
"system/hotspot/hotspot_detailed_view.h",
"system/hotspot/hotspot_feature_pod_controller.cc",
"system/hotspot/hotspot_feature_pod_controller.h",
"system/hotspot/hotspot_info_cache.cc",
@ -3257,6 +3259,7 @@ test("ash_unittests") {
"system/holding_space/test_holding_space_item_views_section.h",
"system/holding_space/test_holding_space_tray_child_bubble.cc",
"system/holding_space/test_holding_space_tray_child_bubble.h",
"system/hotspot/hotspot_detailed_view_unittest.cc",
"system/hotspot/hotspot_feature_pod_controller_unittest.cc",
"system/hotspot/hotspot_info_cache_unittest.cc",
"system/hotspot/hotspot_tray_view_unittest.cc",

@ -2692,6 +2692,27 @@ Connect your device to power.
<message name="IDS_ASH_STATUS_TRAY_HOTSPOT_FEATURE_TILE_TOOLTIP_PROHIBITED_BY_POLICY" desc="The tooltip for the hotspot feature tile of quick settings when hotspot is off and is not allowed to turn on due to prohibited by policy.">
Show hotspot details. Hotspot is blocked by your administrator.
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_SUBLABEL_NO_MOBILE_DATA" desc="The sublabel text in the hotspot detailed view when hotspot is off and not allowed to turn on due to no mobile network connection.">
Connect to mobile data to use hotspot
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED" desc="The tooltip for the info icon in hotspot detailed view when hotspot is off and not allowed to turn on due to the connected mobile data doesn't support hotspot.">
Your mobile network doesn't support hotspot
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY" desc="The tooltip for the info icon in hotspot detailed view when hotspot is off and not allowed to turn on due to enterprise policy.">
This setting is managed by your administrator
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_ON_NO_CONNECTED_DEVICES" desc="The subtext in the hotspot detailed view when hotspot is on and no devices connected to it.">
On, no devices connected
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_TITLE" desc="The label in the hotspot detailed view for the Chromebook Hotspot.">
<ph name="DEVICE_NAME">$1<ex>Chromebook</ex></ph> hotspot
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_TOGGLE_A11Y_TEXT" desc="Accessiblity text for the toggle in the hotspot detailed view.">
Toggle hotspot
</message>
<message name="IDS_ASH_HOTSPOT_DETAILED_VIEW_HOTSPOT_SETTINGS" desc="The tooltip used for the settings button in hotspot detailed page.">
Hotspot settings
</message>
<message name="IDS_ASH_STATUS_TRAY_BLUETOOTH_DISCOVERABLE" desc="Toast shown when a Bluetooth adapter is discoverable.">
"<ph name="NAME">$1<ex>Chromebook</ex></ph>" visible to Bluetooth devices.
</message>

@ -0,0 +1 @@
204f78dd7977206c211fc06221f9a7a8af0ecffe

@ -0,0 +1 @@
ea8d4535c17804bb03999f4e1bb741fccc2673d4

@ -0,0 +1 @@
08b4f60dde31e36372ddcc0494b4326b63493ce6

@ -0,0 +1 @@
449aee573419661208ece4448484059f0f93c781

@ -0,0 +1 @@
a2ffa585697b62e6ba485351fadc9a9080499e2d

@ -0,0 +1 @@
b862e8f636f193f692f5071ef827216a3d466743

@ -0,0 +1 @@
91024c855700197e43d5a8991669b4a2c1447a34

@ -133,6 +133,9 @@ class ASH_PUBLIC_EXPORT SystemTrayClient {
// be any string.
virtual void ShowNetworkSettings(const std::string& network_id) = 0;
// Shows the Hotspot subpage.
virtual void ShowHotspotSubpage() = 0;
// Shows the MultiDevice setup flow dialog.
virtual void ShowMultiDeviceSetup() = 0;

@ -110,6 +110,10 @@ void TestSystemTrayClient::ShowNetworkSettings(const std::string& network_id) {
last_network_settings_network_id_ = network_id;
}
void TestSystemTrayClient::ShowHotspotSubpage() {
show_hotspot_subpage_count_++;
}
void TestSystemTrayClient::ShowMultiDeviceSetup() {
show_multi_device_setup_count_++;
}

@ -58,6 +58,7 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
void ShowThirdPartyVpnCreate(const std::string& extension_id) override;
void ShowArcVpnCreate(const std::string& app_id) override;
void ShowNetworkSettings(const std::string& network_id) override;
void ShowHotspotSubpage() override;
void ShowMultiDeviceSetup() override;
void ShowFirmwareUpdate() override;
void SetLocaleAndExit(const std::string& locale_iso_code) override;
@ -87,6 +88,8 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
return show_bluetooth_pairing_dialog_count_;
}
int show_hotspot_subpage_count() const { return show_hotspot_subpage_count_; }
int show_multi_device_setup_count() const {
return show_multi_device_setup_count_;
}
@ -173,6 +176,7 @@ class ASH_PUBLIC_EXPORT TestSystemTrayClient : public SystemTrayClient {
int show_network_settings_count_ = 0;
int show_bluetooth_settings_count_ = 0;
int show_bluetooth_pairing_dialog_count_ = 0;
int show_hotspot_subpage_count_ = 0;
int show_multi_device_setup_count_ = 0;
int show_connected_devices_settings_count_ = 0;
int show_os_settings_privacy_and_security_count_ = 0;

@ -0,0 +1,265 @@
// 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/hotspot/hotspot_detailed_view.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/rounded_container.h"
#include "ash/style/switch.h"
#include "ash/style/typography.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/tray/detailed_view_delegate.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "base/functional/bind.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
namespace ash {
using hotspot_config::mojom::HotspotAllowStatus;
using hotspot_config::mojom::HotspotInfoPtr;
using hotspot_config::mojom::HotspotState;
namespace {
// Used for setting the insets of broader hotspot entry row.
constexpr auto kToggleRowTriViewInsets = gfx::Insets::VH(8, 24);
bool IsIntermediateState(HotspotState state) {
return state == HotspotState::kDisabling || state == HotspotState::kEnabling;
}
bool IsEnabledOrEnabling(HotspotState state) {
return state == HotspotState::kEnabled || state == HotspotState::kEnabling;
}
} // namespace
HotspotDetailedView::HotspotDetailedView(
DetailedViewDelegate* detailed_view_delegate,
Delegate* delegate)
: TrayDetailedView(detailed_view_delegate), delegate_(delegate) {
CreateTitleRow(IDS_ASH_STATUS_TRAY_HOTSPOT);
CreateScrollableList();
CreateContainer();
}
HotspotDetailedView::~HotspotDetailedView() = default;
void HotspotDetailedView::UpdateViewForHotspot(HotspotInfoPtr hotspot_info) {
// Update the Hotspot icon.
hotspot_icon_->SetImage(ui::ImageModel::FromVectorIcon(
IsEnabledOrEnabling(hotspot_info->state) ? kHotspotOnIcon
: kHotspotOffIcon,
cros_tokens::kCrosSysOnSurface));
UpdateSubText(hotspot_info);
UpdateToggleState(hotspot_info->state, hotspot_info->allow_status);
UpdateExtraIcon(hotspot_info->allow_status);
}
void HotspotDetailedView::HandleViewClicked(views::View* view) {
// Handle clicks on the on/off toggle row.
if (view == entry_row_) {
// The toggle button has the old state, so switch to the opposite state.
ToggleHotspot(!toggle_->GetIsOn());
return;
}
}
void HotspotDetailedView::CreateExtraTitleRowButtons() {
tri_view()->SetContainerVisible(TriView::Container::END, /*visible=*/true);
CHECK(!settings_button_);
settings_button_ = CreateSettingsButton(
base::BindRepeating(&HotspotDetailedView::OnSettingsClicked,
weak_factory_.GetWeakPtr()),
IDS_ASH_HOTSPOT_DETAILED_VIEW_HOTSPOT_SETTINGS);
settings_button_->SetState(TrayPopupUtils::CanOpenWebUISettings()
? views::Button::STATE_NORMAL
: views::Button::STATE_DISABLED);
settings_button_->SetID(
static_cast<int>(HotspotDetailedViewChildId::kSettingsButton));
tri_view()->AddView(TriView::Container::END, settings_button_);
}
void HotspotDetailedView::CreateContainer() {
SetAccessibleName(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_HOTSPOT));
row_container_ =
scroll_content()->AddChildView(std::make_unique<RoundedContainer>(
RoundedContainer::Behavior::kAllRounded));
// Ensure the HoverHighlightView ink drop fills the whole container.
row_container_->SetBorderInsets(gfx::Insets());
entry_row_ = row_container_->AddChildView(
std::make_unique<HoverHighlightView>(/*listener=*/this));
entry_row_->SetFocusBehavior(FocusBehavior::NEVER);
entry_row_->SetID(static_cast<int>(HotspotDetailedViewChildId::kEntryRow));
// The icon image and label text depend on whether hotspot is enabled. They
// are set in UpdateViewForHotspot().
auto hotspot_icon = std::make_unique<views::ImageView>();
hotspot_icon->SetID(
static_cast<int>(HotspotDetailedViewChildId::kHotspotIcon));
hotspot_icon_ = hotspot_icon.get();
entry_row_->AddViewAndLabel(std::move(hotspot_icon), u"");
entry_row_->text_label()->SetText(l10n_util::GetStringFUTF16(
IDS_ASH_HOTSPOT_DETAILED_VIEW_TITLE, ui::GetChromeOSDeviceName()));
entry_row_->text_label()->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
*entry_row_->text_label());
auto toggle = std::make_unique<Switch>(base::BindRepeating(
&HotspotDetailedView::OnToggleClicked, weak_factory_.GetWeakPtr()));
toggle->SetAccessibleName(l10n_util::GetStringUTF16(
IDS_ASH_HOTSPOT_DETAILED_VIEW_TOGGLE_A11Y_TEXT));
toggle->SetID(static_cast<int>(HotspotDetailedViewChildId::kToggle));
toggle_ = toggle.get();
entry_row_->AddRightView(toggle.release());
// Allow the row to be taller than a typical tray menu item.
entry_row_->SetExpandable(true);
entry_row_->tri_view()->SetInsets(kToggleRowTriViewInsets);
}
void HotspotDetailedView::OnSettingsClicked() {
CloseBubble(); // Delete `this`.
Shell::Get()->system_tray_model()->client()->ShowHotspotSubpage();
}
void HotspotDetailedView::OnToggleClicked() {
// The toggle button already has the new state after a click.
ToggleHotspot(toggle_->GetIsOn());
}
void HotspotDetailedView::ToggleHotspot(bool new_state) {
delegate_->OnToggleClicked(new_state);
}
void HotspotDetailedView::UpdateToggleState(
const HotspotState& state,
const HotspotAllowStatus& allow_status) {
toggle_->SetIsOn(IsEnabledOrEnabling(state));
bool enabled = !IsIntermediateState(state) &&
allow_status == HotspotAllowStatus::kAllowed;
entry_row_->SetEnabled(enabled);
}
void HotspotDetailedView::UpdateSubText(const HotspotInfoPtr& hotspot_info) {
std::u16string sub_text;
switch (hotspot_info->state) {
case HotspotState::kEnabled: {
uint32_t client_count = hotspot_info->client_count;
if (client_count == 0) {
sub_text = l10n_util::GetStringUTF16(
IDS_ASH_HOTSPOT_DETAILED_VIEW_ON_NO_CONNECTED_DEVICES);
} else if (client_count == 1) {
sub_text = l10n_util::GetStringUTF16(
IDS_ASH_HOTSPOT_ON_MESSAGE_ONE_CONNECTED_DEVICE);
} else {
sub_text = l10n_util::GetStringFUTF16(
IDS_ASH_HOTSPOT_ON_MESSAGE_MULTIPLE_CONNECTED_DEVICES,
base::NumberToString16(client_count));
}
break;
}
case HotspotState::kDisabled: {
const HotspotAllowStatus allow_status = hotspot_info->allow_status;
if (allow_status == HotspotAllowStatus::kDisallowedNoMobileData) {
sub_text = l10n_util::GetStringUTF16(
IDS_ASH_HOTSPOT_DETAILED_VIEW_SUBLABEL_NO_MOBILE_DATA);
}
break;
}
case HotspotState::kEnabling:
sub_text = l10n_util::GetStringUTF16(
IDS_ASH_STATUS_TRAY_HOTSPOT_STATUS_ENABLING);
break;
case HotspotState::kDisabling:
sub_text = l10n_util::GetStringUTF16(
IDS_ASH_STATUS_TRAY_HOTSPOT_STATUS_DISABLING);
break;
}
if (!sub_text.empty()) {
entry_row_->SetSubText(sub_text);
entry_row_->sub_text_label()->SetVisible(true);
if (hotspot_info->state != HotspotState::kEnabled) {
// If hotspot is not enabled, no need to set primary color for the status
// sublabel text.
return;
}
// Set color for the subtext that shows hotspot is connected.
if (chromeos::features::IsJellyEnabled()) {
entry_row_->sub_text_label()->SetEnabledColorId(
cros_tokens::kCrosSysPositive);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosAnnotation1,
*entry_row_->sub_text_label());
} else {
entry_row_->sub_text_label()->SetEnabledColorId(
kColorAshTextColorPositive);
}
return;
}
// If no subtext is set, previous subtext should be hidden.
if (entry_row_->sub_text_label()) {
entry_row_->sub_text_label()->SetVisible(false);
}
}
void HotspotDetailedView::UpdateExtraIcon(
const HotspotAllowStatus& allow_status) {
if (allow_status == HotspotAllowStatus::kAllowed ||
allow_status == HotspotAllowStatus::kDisallowedNoMobileData) {
RemoveExtraIcon();
return;
}
if (!extra_icon_) {
std::unique_ptr<views::ImageView> extra_icon(
TrayPopupUtils::CreateMainImageView(/*use_wide_layout=*/false));
extra_icon->SetBackground(
views::CreateSolidBackground(SK_ColorTRANSPARENT));
extra_icon->SetID(static_cast<int>(HotspotDetailedViewChildId::kExtraIcon));
extra_icon_ = extra_icon.get();
entry_row_->AddAdditionalRightView(extra_icon.release());
}
bool use_managed_icon =
allow_status == HotspotAllowStatus::kDisallowedByPolicy;
extra_icon_->SetImage(ui::ImageModel::FromVectorIcon(
use_managed_icon ? kSystemTrayManagedIcon : kUnifiedMenuInfoIcon,
kColorAshIconColorPrimary));
extra_icon_->SetTooltipText(l10n_util::GetStringUTF16(
use_managed_icon
? IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY
: IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED));
}
void HotspotDetailedView::RemoveExtraIcon() {
if (extra_icon_ && extra_icon_->parent()) {
extra_icon_->parent()->RemoveChildView(extra_icon_);
extra_icon_ = nullptr;
}
}
BEGIN_METADATA(HotspotDetailedView, TrayDetailedView)
END_METADATA
} // namespace ash

@ -0,0 +1,105 @@
// 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.
#ifndef ASH_SYSTEM_HOTSPOT_HOTSPOT_DETAILED_VIEW_H_
#define ASH_SYSTEM_HOTSPOT_HOTSPOT_DETAILED_VIEW_H_
#include "ash/ash_export.h"
#include "ash/system/tray/tray_detailed_view.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "chromeos/ash/services/hotspot_config/public/mojom/cros_hotspot_config.mojom.h"
#include "ui/base/metadata/metadata_header_macros.h"
namespace views {
class Button;
class ImageView;
} // namespace views
namespace ash {
class DetailedViewDelegate;
class HoverHighlightView;
class RoundedContainer;
class Switch;
// This class defines both the interface used to interact with the detailed
// Hotspot page within the quick settings. This class includes the declaration
// for the delegate interface it uses to propagate user interactions.
class ASH_EXPORT HotspotDetailedView : public TrayDetailedView {
public:
METADATA_HEADER(HotspotDetailedView);
// This class defines the interface that HotspotDetailedView will use to
// propagate user interactions.
class Delegate {
public:
Delegate() = default;
virtual ~Delegate() = default;
virtual void OnToggleClicked(bool new_state) = 0;
};
HotspotDetailedView(DetailedViewDelegate* detailed_view_delegate,
Delegate* delegate);
HotspotDetailedView(const HotspotDetailedView&) = delete;
HotspotDetailedView& operator=(const HotspotDetailedView&) = delete;
~HotspotDetailedView() override;
// Update the hotspot detailed view from the given `hotspot_info`.
void UpdateViewForHotspot(hotspot_config::mojom::HotspotInfoPtr hotspot_info);
// TrayDetailedView:
void HandleViewClicked(views::View* view) override;
void CreateExtraTitleRowButtons() override;
private:
friend class HotspotDetailedViewTest;
// Used for testing. Starts at 1 because view IDs should not be 0.
enum class HotspotDetailedViewChildId {
kInfoButton = 1,
kSettingsButton = 2,
kEntryRow = 3,
kHotspotIcon = 4,
kToggle = 5,
kExtraIcon = 6,
};
// Creates the rounded container, which contains the main on/off toggle.
void CreateContainer();
// Attempts to close the quick settings and open the Hotspot subpage.
void OnSettingsClicked();
// Handles clicks on the Hotspot toggle button.
void OnToggleClicked();
// Handles toggling Hotspot via the UI to `new_state`.
void ToggleHotspot(bool new_state);
void UpdateToggleState(
const hotspot_config::mojom::HotspotState& state,
const hotspot_config::mojom::HotspotAllowStatus& allow_status);
void UpdateSubText(const hotspot_config::mojom::HotspotInfoPtr& hotspot_info);
void UpdateExtraIcon(
const hotspot_config::mojom::HotspotAllowStatus& allow_status);
void RemoveExtraIcon();
const raw_ptr<Delegate, ExperimentalAsh> delegate_;
// Owned by views hierarchy.
raw_ptr<views::Button, ExperimentalAsh> settings_button_ = nullptr;
raw_ptr<RoundedContainer, ExperimentalAsh> row_container_ = nullptr;
raw_ptr<HoverHighlightView, ExperimentalAsh> entry_row_ = nullptr;
raw_ptr<views::ImageView, ExperimentalAsh> hotspot_icon_ = nullptr;
raw_ptr<Switch, ExperimentalAsh> toggle_ = nullptr;
raw_ptr<views::ImageView, ExperimentalAsh> extra_icon_ = nullptr;
base::WeakPtrFactory<HotspotDetailedView> weak_factory_{this};
};
} // namespace ash
#endif // ASH_SYSTEM_HOTSPOT_HOTSPOT_DETAILED_VIEW_H_

@ -0,0 +1,302 @@
// 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/hotspot/hotspot_detailed_view.h"
#include "ash/public/cpp/test/test_system_tray_client.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/rounded_container.h"
#include "ash/style/switch.h"
#include "ash/system/tray/detailed_view_delegate.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr char16_t kHotspotTitle[] = u"Chrome device hotspot";
} // namespace
using hotspot_config::mojom::HotspotAllowStatus;
using hotspot_config::mojom::HotspotInfo;
using hotspot_config::mojom::HotspotState;
class FakeHotspotDetailedViewDelegate : public HotspotDetailedView::Delegate {
public:
FakeHotspotDetailedViewDelegate() = default;
~FakeHotspotDetailedViewDelegate() override = default;
// HotspotDetailedView::Delegate:
void OnToggleClicked(bool new_state) override {
last_toggle_state_ = new_state;
}
bool last_toggle_state_ = false;
};
// This class exists to stub out the CloseBubble() call. This allows tests to
// directly construct the detailed view, without depending on the entire quick
// settings bubble and view hierarchy.
class FakeDetailedViewDelegate : public DetailedViewDelegate {
public:
FakeDetailedViewDelegate()
: DetailedViewDelegate(/*tray_controller=*/nullptr) {}
~FakeDetailedViewDelegate() override = default;
// DetailedViewDelegate:
void CloseBubble() override { ++close_bubble_count_; }
int close_bubble_count_ = 0;
};
class HotspotDetailedViewTest : public AshTestBase {
public:
HotspotDetailedViewTest() = default;
~HotspotDetailedViewTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
auto hotspot_detailed_view = std::make_unique<HotspotDetailedView>(
&detailed_view_delegate_, &hotspot_detailed_view_delegate_);
hotspot_detailed_view_ = hotspot_detailed_view.get();
widget_ = CreateFramelessTestWidget();
widget_->SetFullscreen(true);
widget_->SetContentsView(hotspot_detailed_view.release());
}
void TearDown() override {
widget_.reset();
AshTestBase::TearDown();
}
void UpdateHotspotView(HotspotState state,
HotspotAllowStatus allow_status,
uint32_t client_count = 0) {
auto hotspot_info = HotspotInfo::New();
hotspot_info->state = state;
hotspot_info->allow_status = allow_status;
hotspot_info->client_count = client_count;
hotspot_detailed_view_->UpdateViewForHotspot(std::move(hotspot_info));
}
views::Button* GetSettingsButton() {
return FindViewById<views::Button*>(
HotspotDetailedView::HotspotDetailedViewChildId::kSettingsButton);
}
HoverHighlightView* GetEntryRow() {
return FindViewById<HoverHighlightView*>(
HotspotDetailedView::HotspotDetailedViewChildId::kEntryRow);
}
Switch* GetToggleButton() {
return FindViewById<Switch*>(
HotspotDetailedView::HotspotDetailedViewChildId::kToggle);
}
views::ImageView* GetExtraIcon() {
return FindViewById<views::ImageView*>(
HotspotDetailedView::HotspotDetailedViewChildId::kExtraIcon);
}
void AssertTextLabel(const std::u16string& expected_text) {
HoverHighlightView* entry_row = GetEntryRow();
ASSERT_TRUE(entry_row->text_label());
EXPECT_EQ(expected_text, entry_row->text_label()->GetText());
}
void AssertSubtextLabel(const std::u16string& expected_text) {
HoverHighlightView* entry_row = GetEntryRow();
if (expected_text.empty()) {
EXPECT_FALSE(entry_row->sub_text_label());
return;
}
ASSERT_TRUE(entry_row->sub_text_label());
EXPECT_TRUE(entry_row->sub_text_label()->GetVisible());
EXPECT_EQ(expected_text, entry_row->sub_text_label()->GetText());
}
void AssertEntryRowEnabled(bool expected_enabled) {
HoverHighlightView* entry_row = GetEntryRow();
ASSERT_TRUE(entry_row);
if (expected_enabled) {
EXPECT_TRUE(entry_row->GetEnabled());
return;
}
EXPECT_FALSE(entry_row->GetEnabled());
}
void AssertToggleOn(bool expected_toggle_on) {
Switch* toggle = GetToggleButton();
ASSERT_TRUE(toggle);
if (expected_toggle_on) {
EXPECT_TRUE(toggle->GetIsOn());
return;
}
EXPECT_FALSE(toggle->GetIsOn());
}
protected:
template <class T>
T FindViewById(HotspotDetailedView::HotspotDetailedViewChildId id) {
return static_cast<T>(
hotspot_detailed_view_->GetViewByID(static_cast<int>(id)));
}
std::unique_ptr<views::Widget> widget_;
FakeHotspotDetailedViewDelegate hotspot_detailed_view_delegate_;
FakeDetailedViewDelegate detailed_view_delegate_;
raw_ptr<HotspotDetailedView, ExperimentalAsh> hotspot_detailed_view_ =
nullptr;
};
TEST_F(HotspotDetailedViewTest, PressingSettingsButtonOpensSettings) {
ASSERT_TRUE(hotspot_detailed_view_);
views::Button* settings_button = GetSettingsButton();
ASSERT_TRUE(settings_button);
// Clicking the button at the lock screen does nothing.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::LOCKED);
LeftClickOn(settings_button);
EXPECT_EQ(0, GetSystemTrayClient()->show_hotspot_subpage_count());
EXPECT_EQ(0, detailed_view_delegate_.close_bubble_count_);
// Clicking the button in an active user session opens OS settings.
GetSessionControllerClient()->SetSessionState(
session_manager::SessionState::ACTIVE);
LeftClickOn(settings_button);
EXPECT_EQ(1, GetSystemTrayClient()->show_hotspot_subpage_count());
EXPECT_EQ(1, detailed_view_delegate_.close_bubble_count_);
}
TEST_F(HotspotDetailedViewTest, HotspotEnabledUI) {
UpdateHotspotView(HotspotState::kEnabled, HotspotAllowStatus::kAllowed);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(u"On, no devices connected");
AssertEntryRowEnabled(/*expected_enabled=*/true);
AssertToggleOn(/*expected_toggle_on=*/true);
views::ImageView* extra_icon = GetExtraIcon();
EXPECT_FALSE(extra_icon);
UpdateHotspotView(HotspotState::kEnabled, HotspotAllowStatus::kAllowed, 1);
AssertSubtextLabel(u"1 device connected");
UpdateHotspotView(HotspotState::kEnabled, HotspotAllowStatus::kAllowed, 2);
AssertSubtextLabel(u"2 devices connected");
}
TEST_F(HotspotDetailedViewTest, HotspotEnablingUI) {
UpdateHotspotView(HotspotState::kEnabling, HotspotAllowStatus::kAllowed);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(u"Enabling…");
AssertEntryRowEnabled(/*expected_enabled=*/false);
AssertToggleOn(/*expected_toggle_on=*/true);
views::ImageView* extra_icon = GetExtraIcon();
EXPECT_FALSE(extra_icon);
}
TEST_F(HotspotDetailedViewTest, HotspotDisablingUI) {
UpdateHotspotView(HotspotState::kDisabling, HotspotAllowStatus::kAllowed);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(u"Disabling…");
AssertEntryRowEnabled(/*expected_enabled=*/false);
AssertToggleOn(/*expected_toggle_on=*/false);
views::ImageView* extra_icon = GetExtraIcon();
EXPECT_FALSE(extra_icon);
}
TEST_F(HotspotDetailedViewTest, HotspotDisabledAndAllowedUI) {
UpdateHotspotView(HotspotState::kDisabled, HotspotAllowStatus::kAllowed);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(std::u16string());
AssertEntryRowEnabled(/*expected_enabled=*/true);
AssertToggleOn(/*expected_toggle_on=*/false);
views::ImageView* extra_icon = GetExtraIcon();
EXPECT_FALSE(extra_icon);
}
TEST_F(HotspotDetailedViewTest, HotspotDisabledAndNoMobileNetworkUI) {
UpdateHotspotView(HotspotState::kDisabled,
HotspotAllowStatus::kDisallowedNoMobileData);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(u"Connect to mobile data to use hotspot");
AssertEntryRowEnabled(/*expected_enabled=*/false);
AssertToggleOn(/*expected_toggle_on=*/false);
views::ImageView* extra_icon = GetExtraIcon();
EXPECT_FALSE(extra_icon);
}
TEST_F(HotspotDetailedViewTest,
HotspotDisabledAndMobileNetworkNotSuppportedUI) {
UpdateHotspotView(HotspotState::kDisabled,
HotspotAllowStatus::kDisallowedReadinessCheckFail);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(std::u16string());
views::ImageView* extra_icon = GetExtraIcon();
ASSERT_TRUE(extra_icon);
EXPECT_EQ(u"Your mobile network doesn't support hotspot",
extra_icon->GetTooltipText());
AssertEntryRowEnabled(/*expected_enabled=*/false);
AssertToggleOn(/*expected_toggle_on=*/false);
}
TEST_F(HotspotDetailedViewTest, HotspotDisabledAndBlockedByPolicyUI) {
UpdateHotspotView(HotspotState::kDisabled,
HotspotAllowStatus::kDisallowedByPolicy);
ASSERT_TRUE(hotspot_detailed_view_);
AssertTextLabel(kHotspotTitle);
AssertSubtextLabel(std::u16string());
views::ImageView* extra_icon = GetExtraIcon();
ASSERT_TRUE(extra_icon);
EXPECT_EQ(u"This setting is managed by your administrator",
extra_icon->GetTooltipText());
AssertEntryRowEnabled(/*expected_enabled=*/false);
AssertToggleOn(/*expected_toggle_on=*/false);
}
TEST_F(HotspotDetailedViewTest, PressingEntryRowNotifiesDelegate) {
ASSERT_TRUE(hotspot_detailed_view_);
HoverHighlightView* entry_row = GetEntryRow();
EXPECT_FALSE(hotspot_detailed_view_delegate_.last_toggle_state_);
LeftClickOn(entry_row);
EXPECT_TRUE(hotspot_detailed_view_delegate_.last_toggle_state_);
}
TEST_F(HotspotDetailedViewTest, PressingToggleNotifiesDelegate) {
ASSERT_TRUE(hotspot_detailed_view_);
Switch* toggle = GetToggleButton();
EXPECT_FALSE(toggle->GetIsOn());
EXPECT_FALSE(hotspot_detailed_view_delegate_.last_toggle_state_);
LeftClickOn(toggle);
EXPECT_TRUE(toggle->GetIsOn());
EXPECT_TRUE(hotspot_detailed_view_delegate_.last_toggle_state_);
}
} // namespace ash

@ -639,6 +639,11 @@ void SystemTrayClientImpl::ShowNetworkSettings(const std::string& network_id) {
ShowNetworkSettingsHelper(network_id, false /* show_configure */);
}
void SystemTrayClientImpl::ShowHotspotSubpage() {
ShowSettingsSubPageForActiveUser(
chromeos::settings::mojom::kHotspotSubpagePath);
}
void SystemTrayClientImpl::ShowNetworkSettingsHelper(
const std::string& network_id,
bool show_configure) {

@ -93,6 +93,7 @@ class SystemTrayClientImpl : public ash::SystemTrayClient,
void ShowThirdPartyVpnCreate(const std::string& extension_id) override;
void ShowArcVpnCreate(const std::string& app_id) override;
void ShowNetworkSettings(const std::string& network_id) override;
void ShowHotspotSubpage() override;
void ShowMultiDeviceSetup() override;
void ShowFirmwareUpdate() override;
void SetLocaleAndExit(const std::string& locale_iso_code) override;