// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/scalable_iph/scalable_iph_delegate_impl.h"
#include <memory>
#include <string_view>
#include "apps/launcher.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/login/ui/lock_screen.h"
#include "ash/public/cpp/app_list/app_list_controller.h"
#include "ash/public/cpp/network_config_service.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "ash/root_window_controller.h"
#include "ash/scalable_iph/scalable_iph_ash_notification_view.h"
#include "ash/scalable_iph/wallpaper_ash_notification_view.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/notification_center/message_view_factory.h"
#include "ash/webui/grit/ash_print_management_resources.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "build/buildflag.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/crosapi/crosapi_util.h"
#include "chrome/browser/ash/phonehub/phone_hub_manager_factory.h"
#include "chrome/browser/ash/printing/synced_printers_manager.h"
#include "chrome/browser/ash/printing/synced_printers_manager_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chromeos/ash/components/phonehub/feature_status_provider.h"
#include "chromeos/ash/components/phonehub/phone_hub_manager.h"
#include "chromeos/ash/components/scalable_iph/buildflags.h"
#include "chromeos/ash/components/scalable_iph/iph_session.h"
#include "chromeos/ash/components/scalable_iph/scalable_iph_delegate.h"
#include "chromeos/ash/grit/ash_resources.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/variations/service/variations_service.h"
#include "net/base/url_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "url/gurl.h"
#include "url/url_canon_stdstring.h"
#if BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
#include "chrome/grit/preinstalled_web_apps_resources.h" // nogncheck
#endif // BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
namespace ash {
namespace {
using ::chromeos::network_config::mojom::ConnectionStateType;
using ::chromeos::network_config::mojom::FilterType;
using ::chromeos::network_config::mojom::kNoLimit;
using ::chromeos::network_config::mojom::NetworkFilter;
using ::chromeos::network_config::mojom::NetworkStatePropertiesPtr;
using ::chromeos::network_config::mojom::NetworkType;
using DelegateObserver = ::scalable_iph::ScalableIphDelegate::Observer;
using DelegateSessionState = ::scalable_iph::ScalableIphDelegate::SessionState;
using Action = ::scalable_iph::ScalableIphDelegate::Action;
using NotificationParams =
::scalable_iph::ScalableIphDelegate::NotificationParams;
using NotificationImageType =
::scalable_iph::ScalableIphDelegate::NotificationImageType;
using BubbleIcon = ::scalable_iph::ScalableIphDelegate::BubbleIcon;
using scalable_iph::ActionType;
constexpr char kScalableIphNotificationType[] =
"scalable_iph_notification_type";
constexpr char kWallpaperNotificationType[] = "wallpaper_notification_type";
constexpr char kNotifierId[] = "scalable_iph";
constexpr char kButtonIndex = 0;
constexpr gfx::Size kBubbleIconSizeDip = gfx::Size(60, 60);
constexpr char kHelpAppPerksUrl[] = "chrome://help-app/offers";
constexpr std::string_view kChromebookPerksUrl =
"https://www.google.com/chromebook/perks/";
constexpr std::string_view kChromebookPerksUrlQueryNameId = "id";
constexpr auto kIdsOfPerksMinecraftRealms2023 =
base::MakeFixedFlatMap<std::string_view, std::string_view>(
{{"us", "minecraft.realms.2023"},
{"gb", "minecraft.uk.2023"},
{"ca", "minecraft.realms.ca.2023"},
{"au", "minecraft.realms.au.2023"}});
std::string GetCountryCode() {
return g_browser_process->variations_service()->GetStoredPermanentCountry();
}
GURL GetPerksMinecraftRealmsUrl(std::string_view country_code) {
const auto it = kIdsOfPerksMinecraftRealms2023.find(country_code);
if (it == kIdsOfPerksMinecraftRealms2023.end()) {
return GURL();
}
return net::AppendQueryParameter(GURL(kChromebookPerksUrl),
kChromebookPerksUrlQueryNameId, it->second);
}
const base::flat_map<ActionType, std::string>& GetActionTypeURLs() {
static const base::NoDestructor<base::flat_map<ActionType, std::string>>
action_type_urls(
{{ActionType::kOpenChrome, "chrome://new-tab-page/"},
{ActionType::kOpenPlayStore,
"https://play.google.com/store/games?device=chromebook"},
{ActionType::kOpenGoogleDocs,
"https://docs.google.com/document/?usp=installed_webapp/"},
{ActionType::kOpenGooglePhotos, "https://photos.google.com/"},
{ActionType::kOpenYouTube, "https://www.youtube.com/"},
{ActionType::kOpenChromebookPerksWeb,
"https://www.google.com/chromebook/perks/"},
{ActionType::kOpenChromebookPerksGfnPriority2022,
"https://www.google.com/chromebook/perks/?id=gfn.priority.2022"},
{ActionType::kOpenChromebookPerksMinecraft2023,
"https://www.google.com/chromebook/perks/?id=minecraft.2023"}});
return *action_type_urls;
}
std::string ToString(ConnectionStateType connection_state_type) {
switch (connection_state_type) {
case ConnectionStateType::kOnline:
return "Online";
case ConnectionStateType::kConnected:
return "Connected";
case ConnectionStateType::kPortal:
return "Portal";
case ConnectionStateType::kConnecting:
return "Connecting";
case ConnectionStateType::kNotConnected:
return "NotConnected";
}
}
bool HasOnlineNetwork(const std::vector<NetworkStatePropertiesPtr>& networks,
scalable_iph::Logger* logger) {
SCALABLE_IPH_LOG(logger) << "Checking networks. Size: " << networks.size();
for (const NetworkStatePropertiesPtr& network : networks) {
SCALABLE_IPH_LOG(logger)
<< network->name << ": " << ToString(network->connection_state);
if (network->connection_state == ConnectionStateType::kOnline) {
return true;
}
}
return false;
}
// Adds the given `notification` to the message center after it removes any
// existing notification that has the same ID.
void AddOrReplaceNotification(
std::unique_ptr<message_center::Notification> notification) {
auto* message_center = message_center::MessageCenter::Get();
message_center->RemoveNotification(notification->id(),
/*by_user=*/false);
message_center->AddNotification(std::move(notification));
}
message_center::NotifierId GetNotifierId() {
return message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT, kNotifierId,
NotificationCatalogName::kScalableIphNotification);
}
bool IsAppValidForProfile(Profile* profile, const std::string& app_id) {
if (app_id == arc::kPlayStoreAppId &&
!arc::IsArcPlayStoreEnabledForProfile(profile)) {
return false;
}
if (!arc::IsArcAllowedForProfile(profile)) {
return false;
}
ArcAppListPrefs* const prefs = ArcAppListPrefs::Get(profile);
std::unique_ptr<ArcAppListPrefs::AppInfo> app_info = prefs->GetApp(app_id);
if (!app_info || !app_info->ready) {
return false;
}
return true;
}
void OpenUrlForProfile(Profile* profile,
const GURL& url,
scalable_iph::Logger* logger) {
SCALABLE_IPH_LOG(logger) << "Opening a url with ash::NewWindowDelegate. Url: "
<< url;
ash::NewWindowDelegate::GetPrimary()->OpenUrl(
url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
ash::NewWindowDelegate::Disposition::kNewWindow);
}
int GetResourceId(BubbleIcon icon) {
#if BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
switch (icon) {
case BubbleIcon::kChromeIcon:
return IDR_PRODUCT_LOGO_128;
case BubbleIcon::kGoogleDocsIcon:
return IDR_SCALABLE_IPH_GOOGLE_DOCS_ICON_120_PNG;
case BubbleIcon::kPrintJobsIcon:
return IDR_ASH_PRINT_MANAGEMENT_PRINT_MANAGEMENT_192_PNG;
case BubbleIcon::kYouTubeIcon:
return IDR_SCALABLE_IPH_YOUTUBE_ICON_120_PNG;
case BubbleIcon::kPlayStoreIcon:
return IDR_SCALABLE_IPH_GOOGLE_PLAY_ICON_120_PNG;
case BubbleIcon::kGooglePhotosIcon:
return IDR_SCALABLE_IPH_GOOGLE_PHOTOS_ICON_120_PNG;
case BubbleIcon::kNoIcon:
NOTREACHED();
}
#else
return IDR_PRODUCT_LOGO_128;
#endif // BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
}
std::optional<int> GetResourceId(
NotificationImageType notification_image_type) {
#if BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
switch (notification_image_type) {
case NotificationImageType::kNoImage:
return std::nullopt;
case NotificationImageType::kWallpaper:
return IDR_SCALABLE_IPH_NOTIFICATION_WALLPAPER_1_PNG;
case NotificationImageType::kMinecraft:
return IDR_SCALABLE_IPH_NOTIFICATION_MINECRAFT_1x_PNG;
}
#else
return std::nullopt;
#endif // BUILDFLAG(ENABLE_CROS_SCALABLE_IPH)
}
std::optional<std::string> GetNotificationCustomViewType(
const NotificationParams& params) {
if (params.image_type == NotificationImageType::kWallpaper) {
return kWallpaperNotificationType;
}
if (params.summary_text == scalable_iph::ScalableIphDelegate::
NotificationSummaryText::kWelcomeTips) {
return kScalableIphNotificationType;
}
return std::nullopt;
}
class ScalableIphNotificationDelegate
: public message_center::NotificationDelegate {
public:
ScalableIphNotificationDelegate(
std::unique_ptr<scalable_iph::IphSession> iph_session,
std::string notification_id,
Action action)
: iph_session_(std::move(iph_session)),
notification_id_(notification_id),
action_(action) {}
// message_center::NotificationDelegate:
void Click(const std::optional<int>& button_index,
const std::optional<std::u16string>& reply) override {
if (!button_index.has_value() || button_index.value() != kButtonIndex) {
return;
}
iph_session_->PerformAction(action_.action_type, action_.iph_event_name);
message_center::MessageCenter::Get()->RemoveNotification(notification_id_,
/*by_user=*/false);
}
private:
~ScalableIphNotificationDelegate() override = default;
std::unique_ptr<scalable_iph::IphSession> iph_session_;
std::string notification_id_;
Action action_;
};
DelegateSessionState GetDelegateSessionState(
session_manager::SessionState state) {
switch (state) {
case session_manager::SessionState::ACTIVE:
return DelegateSessionState::kActive;
case session_manager::SessionState::LOCKED:
return DelegateSessionState::kLocked;
default:
return DelegateSessionState::kOther;
}
}
} // namespace
ScalableIphDelegateImpl::ScalableIphDelegateImpl(Profile* profile,
scalable_iph::Logger* logger)
: profile_(profile), logger_(logger) {
CHECK(profile_);
CHECK(logger_);
SCALABLE_IPH_LOG(GetLogger()) << "Initializing ScalableIphDelegateImpl";
GetNetworkConfigService(
remote_cros_network_config_.BindNewPipeAndPassReceiver());
remote_cros_network_config_->AddObserver(
receiver_cros_network_config_observer_.BindNewPipeAndPassRemote());
QueryOnlineNetworkState();
shell_observer_.Observe(Shell::Get());
auto* session_controller = Shell::Get()->session_controller();
CHECK(session_controller);
session_observer_.Observe(session_controller);
auto* power_manager_client = chromeos::PowerManagerClient::Get();
CHECK(power_manager_client);
power_manager_client_observer_.Observe(power_manager_client);
AppListController* app_list_controller = AppListController::Get();
CHECK(app_list_controller);
app_list_controller_observer_.Observe(app_list_controller);
MessageViewFactory::SetCustomNotificationViewFactory(
kScalableIphNotificationType,
base::BindRepeating(&ScalableIphAshNotificationView::CreateView));
MessageViewFactory::SetCustomNotificationViewFactory(
kWallpaperNotificationType,
base::BindRepeating(&WallpaperAshNotificationView::CreateWithPreview));
synced_printers_manager_ =
SyncedPrintersManagerFactory::GetForBrowserContext(profile);
CHECK(synced_printers_manager_);
synced_printers_manager_observer_.Observe(synced_printers_manager_);
MaybeNotifyHasSavedPrinters();
if (features::IsCrossDeviceFeatureSuiteAllowed()) {
DCHECK(ash::Shell::Get()->system_tray_model()->phone_hub_manager())
<< "PhoneHubManager is expected to be initialized at a specific "
"timing. "
"See a comment in "
"PhoneHubManagerFactory::ServiceIsCreatedWithBrowserContext. Below "
"PhoneHubManagerFactory::GetForProfile will lazy create a "
"PhoneHubManager. It should be fine as ScalableIph is also "
"initialized at the same timing. But it's ideal if PhoneHubManager "
"is "
"created at the intended initialization timing instead of our call, "
"i.e. PhoneHubManager should be already created at this point.";
phonehub::PhoneHubManager* phone_hub_manager =
phonehub::PhoneHubManagerFactory::GetForProfile(profile);
CHECK(phone_hub_manager);
feature_status_provider_ = phone_hub_manager->GetFeatureStatusProvider();
CHECK(feature_status_provider_);
feature_status_provider_observer_.Observe(feature_status_provider_);
MaybeNotifyPhoneHubOnboardingEligibility();
}
}
// Remember NOT to interact with `iph_session` from the destructor. See the
// comment of `ScalableIphDelegate::ShowBubble` for details.
ScalableIphDelegateImpl::~ScalableIphDelegateImpl() {
// Remove the custom notification view factories.
MessageViewFactory::ClearCustomNotificationViewFactory(
kScalableIphNotificationType);
MessageViewFactory::ClearCustomNotificationViewFactory(
kWallpaperNotificationType);
}
bool ScalableIphDelegateImpl::ShowBubble(
const scalable_iph::ScalableIphDelegate::BubbleParams& params,
std::unique_ptr<scalable_iph::IphSession> iph_session) {
if (!IsEligibleAction(params.button.action.action_type)) {
SCALABLE_IPH_LOG(GetLogger())
<< "Specified action is not eligible -> Not showing a bubble.";
return false;
}
// TODO(b/323426306): move this out to //ash/scalable_iph.
SCALABLE_IPH_LOG(GetLogger()) << "Show bubble: " << params;
// It will be no-op if the `bubble_id_` is an empty string when the first time
// to show a bubble.
ash::AnchoredNudgeManager::Get()->Cancel(bubble_id_);
bubble_id_ = params.bubble_id;
bubble_iph_session_ = std::move(iph_session);
ShelfAppButton* anchor_view = nullptr;
if (!params.anchor_view_app_id.empty()) {
anchor_view =
Shell::GetPrimaryRootWindowController()
->shelf()
->hotseat_widget()
->GetShelfView()
->GetShelfAppButton(ash::ShelfID(params.anchor_view_app_id));
if (!anchor_view) {
// In the case that the specified app ID cannot be found on the shelf,
// the bubble can't be anchored and will not be shown.
SCALABLE_IPH_LOG(GetLogger())
<< "Unable to find a view for specified anchor view app id. Anchor "
"view app id: "
<< params.anchor_view_app_id << " -> Not showing a bubble.";
bubble_iph_session_.reset();
bubble_id_ = "";
return false;
}
}
ash::AnchoredNudgeData nudge_data(
params.bubble_id, NudgeCatalogName::kScalableIphBubble,
base::UTF8ToUTF16(params.text), /*anchor_view=*/anchor_view);
if (!params.title.empty()) {
nudge_data.title_text = base::UTF8ToUTF16(params.title);
}
// Currently, the help app on the shelf is the only view to which a bubble
// will be anchored to. Therefore, if the anchor_view is non-null, the
// nudge should be anchored to shelf. Once bubbles fully support anchor views,
// this behavior may change.
if (anchor_view) {
SCALABLE_IPH_LOG(GetLogger()) << "Anchoring bubble UI to the shelf.";
nudge_data.anchored_to_shelf = true;
}
if (!params.button.text.empty()) {
nudge_data.primary_button_text = base::UTF8ToUTF16(params.button.text);
nudge_data.primary_button_callback = base::BindRepeating(
&ScalableIphDelegateImpl::OnNudgeButtonClicked,
weak_ptr_factory_.GetWeakPtr(), params.bubble_id, params.button.action);
}
nudge_data.dismiss_callback =
base::BindRepeating(&ScalableIphDelegateImpl::OnNudgeDismissed,
weak_ptr_factory_.GetWeakPtr(), params.bubble_id);
if (params.icon != BubbleIcon::kNoIcon) {
gfx::ImageSkia* image =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
GetResourceId(params.icon));
gfx::ImageSkia resized_image = gfx::ImageSkiaOperations::CreateResizedImage(
*image, skia::ImageOperations::RESIZE_BEST, kBubbleIconSizeDip);
resized_image.EnsureRepsForSupportedScales();
nudge_data.image_model = ui::ImageModel::FromImageSkia(resized_image);
}
ash::AnchoredNudgeManager::Get()->Show(nudge_data);
return true;
}
bool ScalableIphDelegateImpl::ShowNotification(
const NotificationParams& params,
std::unique_ptr<scalable_iph::IphSession> iph_session) {
if (!IsEligibleAction(params.button.action.action_type)) {
SCALABLE_IPH_LOG(GetLogger())
<< "Specified action is not eligible -> Not showing a notification.";
return false;
}
// TODO(b/323426306): move this out to //ash/scalable_iph.
SCALABLE_IPH_LOG(GetLogger()) << "Show notification: " << params;
std::string notification_source_name = params.source;
std::string notification_title = params.title;
std::string notification_text = params.text;
message_center::RichNotificationData rich_notification_data;
CHECK(!params.button.text.empty())
<< "Scalable IPH notification should have a button";
std::string button_text = params.button.text;
message_center::ButtonInfo button_info;
button_info.title = base::UTF8ToUTF16(button_text);
rich_notification_data.buttons.push_back(button_info);
std::optional<int> notification_image_id = GetResourceId(params.image_type);
if (notification_image_id) {
rich_notification_data.image =
ui::ResourceBundle::GetSharedInstance().GetImageNamed(
notification_image_id.value());
}
const gfx::VectorIcon* icon = &gfx::kNoneIcon;
if (params.icon == ScalableIphDelegate::NotificationIcon::kRedeem) {
icon = &chromeos::kRedeemIcon;
}
std::optional<std::string> custom_view_type =
GetNotificationCustomViewType(params);
std::unique_ptr<message_center::Notification> notification =
ash::CreateSystemNotificationPtr(
custom_view_type ? message_center::NOTIFICATION_TYPE_CUSTOM
: message_center::NOTIFICATION_TYPE_SIMPLE,
params.notification_id, base::UTF8ToUTF16(notification_title),
base::UTF8ToUTF16(notification_text),
base::UTF8ToUTF16(notification_source_name), GURL(), GetNotifierId(),
rich_notification_data,
base::MakeRefCounted<ScalableIphNotificationDelegate>(
std::move(iph_session), params.notification_id,
params.button.action),
*icon, message_center::SystemNotificationWarningLevel::NORMAL);
if (custom_view_type) {
notification->set_custom_view_type(custom_view_type.value());
}
AddOrReplaceNotification(std::move(notification));
return true;
}
void ScalableIphDelegateImpl::AddObserver(DelegateObserver* observer) {
observers_.AddObserver(observer);
}
void ScalableIphDelegateImpl::RemoveObserver(DelegateObserver* observer) {
observers_.RemoveObserver(observer);
}
bool ScalableIphDelegateImpl::IsOnline() {
return has_online_network_;
}
int ScalableIphDelegateImpl::ClientAgeInDays() {
const base::Time& creation_time = profile_->GetCreationTime();
const base::TimeDelta& delta = base::Time::Now() - creation_time;
return delta.InDaysFloored();
}
void ScalableIphDelegateImpl::PerformActionForScalableIph(
ActionType action_type) {
SCALABLE_IPH_LOG(GetLogger())
<< "Performing action: Action type: " << action_type;
switch (action_type) {
case ActionType::kOpenChrome: {
OpenUrlForProfile(profile_,
GURL(GetActionTypeURLs().at(ActionType::kOpenChrome)),
GetLogger());
break;
}
case ActionType::kOpenPersonalizationApp: {
SCALABLE_IPH_LOG(GetLogger())
<< "Opening ash::SystemWebAppType::PERSONALIZATION via "
"ash::LaunchSystemWebAppAsync.";
ash::LaunchSystemWebAppAsync(profile_,
ash::SystemWebAppType::PERSONALIZATION);
break;
}
case ActionType::kOpenPlayStore: {
bool app_launched = false;
if (IsAppValidForProfile(profile_, arc::kPlayStoreAppId)) {
app_launched = arc::LaunchApp(
profile_, arc::kPlayStoreAppId, ui::EF_NONE,
arc::UserInteractionType::APP_STARTED_FROM_OTHER_APP);
SCALABLE_IPH_LOG(GetLogger())
<< "Opening Play Store Android app. App launched: " << app_launched;
}
if (!app_launched) {
OpenUrlForProfile(
profile_, GURL(GetActionTypeURLs().at(ActionType::kOpenPlayStore)),
GetLogger());
}
break;
}
case ActionType::kOpenGoogleDocs: {
OpenUrlForProfile(
profile_, GURL(GetActionTypeURLs().at(ActionType::kOpenGoogleDocs)),
GetLogger());
break;
}
case ActionType::kOpenGooglePhotos: {
bool app_launched = false;
if (IsAppValidForProfile(profile_, arc::kGooglePhotosAppId)) {
app_launched = arc::LaunchApp(
profile_, arc::kGooglePhotosAppId, ui::EF_NONE,
arc::UserInteractionType::APP_STARTED_FROM_OTHER_APP);
SCALABLE_IPH_LOG(GetLogger())
<< "Opening Google Photos Android app. App launched: "
<< app_launched;
}
if (!app_launched) {
OpenUrlForProfile(
profile_,
GURL(GetActionTypeURLs().at(ActionType::kOpenGooglePhotos)),
GetLogger());
}
break;
}
case ActionType::kOpenSettingsPrinter: {
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
profile_, chromeos::settings::mojom::kPrintingDetailsSubpagePath);
SCALABLE_IPH_LOG(GetLogger())
<< "Opening OSSettings kPrintingDetailsSubpagePath";
break;
}
case ActionType::kOpenPhoneHub: {
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(
profile_, chromeos::settings::mojom::kMultiDeviceSectionPath);
SCALABLE_IPH_LOG(GetLogger())
<< "Opening OSSettings kMultiDeviceSectionPath";
break;
}
case ActionType::kOpenYouTube: {
if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
profile_) &&
IsAppValidForProfile(profile_, extension_misc::kYoutubePwaAppId)) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
proxy->LaunchAppWithUrl(
extension_misc::kYoutubePwaAppId,
apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/true),
GURL(GetActionTypeURLs().at(ActionType::kOpenYouTube)),
apps::LaunchSource::kFromOtherApp,
std::make_unique<apps::WindowInfo>(display::kDefaultDisplayId));
SCALABLE_IPH_LOG(GetLogger()) << "Opening YouTube app via AppService.";
} else {
OpenUrlForProfile(
profile_, GURL(GetActionTypeURLs().at(ActionType::kOpenYouTube)),
GetLogger());
}
break;
}
case ActionType::kOpenFileManager: {
ash::NewWindowDelegate::GetPrimary()->OpenFileManager();
SCALABLE_IPH_LOG(GetLogger()) << "Opening file manager.";
break;
}
case ActionType::kOpenHelpAppPerks: {
SCALABLE_IPH_LOG(GetLogger())
<< "Opening ash::SystemWebAppType::HELP via "
"ash::LaunchSystemWebAppAsync for url: "
<< kHelpAppPerksUrl;
SystemAppLaunchParams system_app_launch_params;
system_app_launch_params.url = GURL(kHelpAppPerksUrl);
ash::LaunchSystemWebAppAsync(profile_, ash::SystemWebAppType::HELP,
system_app_launch_params);
break;
}
case ActionType::kOpenChromebookPerksWeb: {
OpenUrlForProfile(
profile_,
GURL(GetActionTypeURLs().at(ActionType::kOpenChromebookPerksWeb)),
GetLogger());
break;
}
case ActionType::kOpenChromebookPerksGfnPriority2022: {
OpenUrlForProfile(profile_,
GURL(GetActionTypeURLs().at(
ActionType::kOpenChromebookPerksGfnPriority2022)),
GetLogger());
break;
}
case ActionType::kOpenChromebookPerksMinecraft2023: {
OpenUrlForProfile(profile_,
GURL(GetActionTypeURLs().at(
ActionType::kOpenChromebookPerksMinecraft2023)),
GetLogger());
break;
}
case ActionType::kOpenChromebookPerksMinecraftRealms2023: {
GURL perks_url = GetPerksMinecraftRealmsUrl(GetCountryCode());
CHECK(!perks_url.is_empty())
<< "No perks url found for " << GetCountryCode()
<< ". Unable to perform an action. This must not happen as "
"eligibility of an action must be checked by `IsEligibleAction` "
"before showing it in a UI.";
CHECK(perks_url.is_valid())
<< "Invalid url is provided. Url is managed on the client side. We "
"use CHECK as this is a client side invariant.";
OpenUrlForProfile(profile_, perks_url, GetLogger());
break;
}
case ActionType::kOpenLauncher:
case ActionType::kInvalid: {
DLOG(WARNING)
<< "Action type does not have an implemented call-to-action.";
return;
}
}
}
void ScalableIphDelegateImpl::OnActiveNetworksChanged(
std::vector<NetworkStatePropertiesPtr> networks) {
SetHasOnlineNetwork(HasOnlineNetwork(networks, GetLogger()));
}
void ScalableIphDelegateImpl::OnShellDestroying() {
app_list_controller_observer_.Reset();
power_manager_client_observer_.Reset();
session_observer_.Reset();
shell_observer_.Reset();
}
void ScalableIphDelegateImpl::OnSessionStateChanged(
session_manager::SessionState state) {
NotifySessionStateChanged(GetDelegateSessionState(state));
}
void ScalableIphDelegateImpl::SuspendDone(base::TimeDelta sleep_duration) {
// Do not record event when the lock screen is enabled.
if (ash::LockScreen::HasInstance()) {
SCALABLE_IPH_LOG(GetLogger()) << "Observed SuspendDone. But do nothing as "
"the lock screen is enabled.";
return;
}
SCALABLE_IPH_LOG(GetLogger())
<< "Observed SuspendDone. Notifying SuspendDoneWithoutLockScreen.";
NotifySuspendDoneWithoutLockScreen();
}
void ScalableIphDelegateImpl::OnAppListVisibilityChanged(bool shown,
int64_t display_id) {
SCALABLE_IPH_LOG(GetLogger())
<< "App list visibility changed. Shown: " << shown
<< " Display Id: " << display_id;
for (DelegateObserver& observer : observers_) {
observer.OnAppListVisibilityChanged(shown);
}
}
void ScalableIphDelegateImpl::OnSavedPrintersChanged() {
MaybeNotifyHasSavedPrinters();
}
void ScalableIphDelegateImpl::OnFeatureStatusChanged() {
CHECK(feature_status_provider_observer_.IsObservingSource(
feature_status_provider_));
SCALABLE_IPH_LOG(GetLogger()) << "Phone hub feature status changed observer "
"gets called. Going to check the status.";
MaybeNotifyPhoneHubOnboardingEligibility();
}
void ScalableIphDelegateImpl::SetFakeFeatureStatusProviderForTesting(
phonehub::FeatureStatusProvider* feature_status_provider) {
CHECK(feature_status_provider_observer_.IsObserving())
<< "feature_status_provider_observer_ should be observing a real object.";
CHECK(!feature_status_provider_observer_.IsObservingSource(
feature_status_provider))
<< "feature_status_provider_observer_ is already observing a fake.";
feature_status_provider_ = feature_status_provider;
feature_status_provider_observer_.Reset();
feature_status_provider_observer_.Observe(feature_status_provider_);
MaybeNotifyPhoneHubOnboardingEligibility();
}
bool ScalableIphDelegateImpl::IsEligibleAction(
scalable_iph::ActionType action_type) {
if (action_type ==
scalable_iph::ActionType::kOpenChromebookPerksMinecraftRealms2023) {
GURL perks_url = GetPerksMinecraftRealmsUrl(GetCountryCode());
if (perks_url.is_empty()) {
SCALABLE_IPH_LOG(GetLogger())
<< action_type << " is not eligible for " << GetCountryCode();
return false;
}
}
return true;
}
void ScalableIphDelegateImpl::SetHasOnlineNetwork(bool has_online_network) {
if (has_online_network_ == has_online_network) {
SCALABLE_IPH_LOG(GetLogger())
<< "Has online network state is set to " << has_online_network
<< ". But do nothing as it's the same with the current internal state.";
return;
}
SCALABLE_IPH_LOG(GetLogger())
<< "Has online network state has changed from " << has_online_network_
<< " to " << has_online_network << ". Notifying the state change.";
has_online_network_ = has_online_network;
for (DelegateObserver& observer : observers_) {
observer.OnConnectionChanged(has_online_network_);
}
}
void ScalableIphDelegateImpl::QueryOnlineNetworkState() {
SCALABLE_IPH_LOG(GetLogger()) << "Querying network state.";
remote_cros_network_config_->GetNetworkStateList(
NetworkFilter::New(FilterType::kActive, NetworkType::kAll, kNoLimit),
base::BindOnce(&ScalableIphDelegateImpl::OnNetworkStateList,
weak_ptr_factory_.GetWeakPtr()));
}
void ScalableIphDelegateImpl::OnNetworkStateList(
std::vector<NetworkStatePropertiesPtr> networks) {
SCALABLE_IPH_LOG(GetLogger()) << "Received network state list.";
SetHasOnlineNetwork(HasOnlineNetwork(networks, GetLogger()));
}
void ScalableIphDelegateImpl::NotifySessionStateChanged(
DelegateSessionState session_state) {
for (DelegateObserver& observer : observers_) {
observer.OnSessionStateChanged(session_state);
}
}
void ScalableIphDelegateImpl::NotifySuspendDoneWithoutLockScreen() {
for (DelegateObserver& observer : observers_) {
observer.OnSuspendDoneWithoutLockScreen();
}
}
void ScalableIphDelegateImpl::MaybeNotifyHasSavedPrinters() {
const bool has_saved_printers =
!synced_printers_manager_->GetSavedPrinters().empty();
if (has_saved_printers_ == has_saved_printers) {
SCALABLE_IPH_LOG(GetLogger())
<< "Checked has saved printers status. Do nothing as it matches with "
"the current internal state. Has saved printers: "
<< has_saved_printers_;
return;
}
SCALABLE_IPH_LOG(GetLogger())
<< "Has saved printers status has changed from " << has_saved_printers_
<< " to " << has_saved_printers << ". Notifying the status change.";
has_saved_printers_ = has_saved_printers;
for (DelegateObserver& observer : observers_) {
observer.OnHasSavedPrintersChanged(has_saved_printers_);
}
}
void ScalableIphDelegateImpl::MaybeNotifyPhoneHubOnboardingEligibility() {
CHECK(feature_status_provider_);
phonehub::FeatureStatus feature_status =
feature_status_provider_->GetStatus();
// `kDisabled` means that a user can enable phone hub via settings. It means
// that a user has an eligible phone.
const bool phonehub_onboarding_eligible =
feature_status == phonehub::FeatureStatus::kEligiblePhoneButNotSetUp ||
feature_status == phonehub::FeatureStatus::kDisabled;
SCALABLE_IPH_LOG(GetLogger())
<< "Checking phone hub feature status. Feature status: " << feature_status
<< ". Phone hub onboarding eligible: " << phonehub_onboarding_eligible;
if (phonehub_onboarding_eligible_ == phonehub_onboarding_eligible) {
SCALABLE_IPH_LOG(GetLogger())
<< "Do nothing as there is no change in phone hub onboarding eligible. "
"Phone hub onboarding eligible: "
<< phonehub_onboarding_eligible_;
return;
}
SCALABLE_IPH_LOG(GetLogger())
<< "Phone hub onboarding eligible has changed. Notifying observers. "
"Phone hub onboarding eligible: from: "
<< phonehub_onboarding_eligible_
<< " to: " << phonehub_onboarding_eligible;
phonehub_onboarding_eligible_ = phonehub_onboarding_eligible;
for (DelegateObserver& observer : observers_) {
observer.OnPhoneHubOnboardingEligibleChanged(phonehub_onboarding_eligible_);
}
}
void ScalableIphDelegateImpl::OnNudgeButtonClicked(const std::string& bubble_id,
Action action) {
SCALABLE_IPH_LOG(GetLogger())
<< "A button in a bubble gets clicked. Bubble id: " << bubble_id
<< " Action: " << action;
if (bubble_id_ != bubble_id) {
SCALABLE_IPH_LOG(GetLogger())
<< "Bubble id " << bubble_id << " is an obsolete id.";
DCHECK(false) << "Callback for an obsolete bubble id gets called "
<< bubble_id;
return;
}
bubble_iph_session_->PerformAction(action.action_type, action.iph_event_name);
}
void ScalableIphDelegateImpl::OnNudgeDismissed(const std::string& bubble_id) {
SCALABLE_IPH_LOG(GetLogger())
<< "A bubble gets dismissed. Bubble id: " << bubble_id;
if (bubble_id_ != bubble_id) {
SCALABLE_IPH_LOG(GetLogger())
<< "Bubble id " << bubble_id
<< " is an obsolete id. Current active bubble id is " << bubble_id_;
return;
}
bubble_iph_session_.reset();
bubble_id_ = "";
}
} // namespace ash