[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:

committed by
Chromium LUCI CQ

parent
7286243f09
commit
298498c949
ash
BUILD.gnash_strings.grd
ash_strings_grd
IDS_ASH_HOTSPOT_DETAILED_VIEW_HOTSPOT_SETTINGS.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_ON_NO_CONNECTED_DEVICES.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_SUBLABEL_NO_MOBILE_DATA.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_TITLE.png.sha1IDS_ASH_HOTSPOT_DETAILED_VIEW_TOGGLE_A11Y_TEXT.png.sha1
public
system
chrome/browser/ui/ash
@ -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
|
1
ash/ash_strings_grd/IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED.png.sha1
Normal file
1
ash/ash_strings_grd/IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_MOBILE_DATA_NOT_SUPPORTED.png.sha1
Normal file
@ -0,0 +1 @@
|
||||
ea8d4535c17804bb03999f4e1bb741fccc2673d4
|
1
ash/ash_strings_grd/IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY.png.sha1
Normal file
1
ash/ash_strings_grd/IDS_ASH_HOTSPOT_DETAILED_VIEW_INFO_TOOLTIP_PROHIBITED_BY_POLICY.png.sha1
Normal file
@ -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;
|
||||
|
265
ash/system/hotspot/hotspot_detailed_view.cc
Normal file
265
ash/system/hotspot/hotspot_detailed_view.cc
Normal file
@ -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
|
105
ash/system/hotspot/hotspot_detailed_view.h
Normal file
105
ash/system/hotspot/hotspot_detailed_view.h
Normal file
@ -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_
|
302
ash/system/hotspot/hotspot_detailed_view_unittest.cc
Normal file
302
ash/system/hotspot/hotspot_detailed_view_unittest.cc
Normal file
@ -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;
|
||||
|
Reference in New Issue
Block a user