blob: 5bff6eacba4adc8adf5f96d2aaacfb3b0bee644b [file] [log] [blame]
// Copyright 2013 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/ui/ash/shelf/chrome_shelf_controller.h"
#include <memory>
#include <set>
#include <utility>
#include "ash/components/arc/arc_prefs.h"
#include "ash/components/arc/arc_util.h"
#include "ash/constants/app_types.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/metrics/login_unlock_throughput_recorder.h"
#include "ash/public/cpp/multi_user_window_manager.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_prefs.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/window_animation_types.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.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/extension_apps_utils.h"
#include "chrome/browser/apps/icon_standardizer.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/app_list/app_service/app_service_app_icon_loader.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/app_list/md_icon_normalizer.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/extensions/chrome_app_icon_loader.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/prefs/pref_service_syncable_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/apps/app_info_dialog.h"
#include "chrome/browser/ui/ash/app_icon_color_cache.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/ash/session_controller_client_impl.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_arc_tracker.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/app_service/shelf_app_service_app_updater.h"
#include "chrome/browser/ui/ash/shelf/app_shortcut_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/app_window_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/browser_app_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/browser_app_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/browser_shortcut_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/browser_status_monitor.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_item_factory.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_prefs.h"
#include "chrome/browser/ui/ash/shelf/shelf_controller_helper.h"
#include "chrome/browser/ui/ash/shelf/shelf_extension_app_updater.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/webui/settings/ash/app_management/app_management_uma.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/account_id/account_id.h"
#include "components/app_constants/constants.h"
#include "components/favicon/content/content_favicon_driver.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/management_policy.h"
#include "extensions/common/extension.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/types/display_constants.h"
#include "ui/resources/grit/ui_resources.h"
using app_constants::kChromeAppId;
namespace {
// Returns true if the given |item| has a pinned shelf item type.
bool ItemTypeIsPinned(const ash::ShelfItem& item) {
return ash::IsPinnedShelfItemType(item.type);
}
// Invoked on a worker thread to create standard icon image.
gfx::ImageSkia CreateStandardImageOnWorkerThread(const gfx::ImageSkia& image) {
gfx::ImageSkia standard_image = apps::CreateStandardIconImage(image);
if (!standard_image.isNull())
standard_image.MakeThreadSafe();
return standard_image;
}
// Report shelf buttons initialized to LoginUnlockThroughputRecorder.
void ReportInitShelfIconList(const ash::ShelfModel* model) {
// Shell is not always initializaed in tests.
if (!ash::Shell::HasInstance())
return;
ash::Shell::Get()->login_unlock_throughput_recorder()->InitShelfIconList(
model);
}
// Report shelf buttons updated to LoginUnlockThroughputRecorder.
void ReportUpdateShelfIconList(const ash::ShelfModel* model) {
// Shell is not always initializaed in tests.
if (!ash::Shell::HasInstance())
return;
ash::Shell::Get()->login_unlock_throughput_recorder()->UpdateShelfIconList(
model);
}
} // namespace
// A class to get events from ChromeOS when a user gets changed or added.
class ChromeShelfControllerUserSwitchObserver
: public user_manager::UserManager::UserSessionStateObserver {
public:
explicit ChromeShelfControllerUserSwitchObserver(
ChromeShelfController* controller)
: controller_(controller) {
DCHECK(user_manager::UserManager::IsInitialized());
user_manager::UserManager::Get()->AddSessionStateObserver(this);
}
ChromeShelfControllerUserSwitchObserver(
const ChromeShelfControllerUserSwitchObserver&) = delete;
ChromeShelfControllerUserSwitchObserver& operator=(
const ChromeShelfControllerUserSwitchObserver&) = delete;
~ChromeShelfControllerUserSwitchObserver() override {
user_manager::UserManager::Get()->RemoveSessionStateObserver(this);
}
// user_manager::UserManager::UserSessionStateObserver overrides:
void UserAddedToSession(const user_manager::User* added_user) override;
// ChromeShelfControllerUserSwitchObserver:
void OnUserProfileReadyToSwitch(Profile* profile);
private:
// Add a user to the session.
void AddUser(Profile* profile);
// The owning ChromeShelfController.
raw_ptr<ChromeShelfController, ExperimentalAsh> controller_;
// Users which were just added to the system, but which profiles were not yet
// (fully) loaded.
std::set<std::string> added_user_ids_waiting_for_profiles_;
};
void ChromeShelfControllerUserSwitchObserver::UserAddedToSession(
const user_manager::User* active_user) {
Profile* profile =
multi_user_util::GetProfileFromAccountId(active_user->GetAccountId());
// If we do not have a profile yet, we postpone forwarding the notification
// until it is loaded.
if (!profile) {
added_user_ids_waiting_for_profiles_.insert(
active_user->GetAccountId().GetUserEmail());
} else {
AddUser(profile);
}
}
void ChromeShelfControllerUserSwitchObserver::OnUserProfileReadyToSwitch(
Profile* profile) {
if (!added_user_ids_waiting_for_profiles_.empty()) {
// Check if the profile is from a user which was on the waiting list.
// TODO(alemate): added_user_ids_waiting_for_profiles_ should be
// a set<AccountId>
std::string user_id =
multi_user_util::GetAccountIdFromProfile(profile).GetUserEmail();
std::set<std::string>::iterator it =
base::ranges::find(added_user_ids_waiting_for_profiles_, user_id);
if (it != added_user_ids_waiting_for_profiles_.end()) {
added_user_ids_waiting_for_profiles_.erase(it);
AddUser(profile->GetOriginalProfile());
}
}
}
void ChromeShelfControllerUserSwitchObserver::AddUser(Profile* profile) {
MultiUserWindowManagerHelper::GetInstance()->AddUser(profile);
controller_->AdditionalUserAddedToSession(profile->GetOriginalProfile());
}
// static
ChromeShelfController* ChromeShelfController::instance_ = nullptr;
ChromeShelfController::ChromeShelfController(Profile* profile,
ash::ShelfModel* model,
ChromeShelfItemFactory* creator)
: model_(model),
shelf_item_factory_(creator),
shelf_prefs_(std::make_unique<ChromeShelfPrefs>(profile)) {
DCHECK(!instance_);
instance_ = this;
CHECK(model_);
if (!profile) {
// If no profile was passed, we take the currently active profile and use it
// as the owner of the current desktop.
// Use the original profile as on chromeos we may get a temporary off the
// record profile, unless in guest session (where off the record profile is
// the right one).
profile = ProfileManager::GetActiveUserProfile();
if (!profile->IsGuestSession() && !profile->IsSystemProfile())
profile = profile->GetOriginalProfile();
}
if (chrome::SettingsWindowManager::UseDeprecatedSettingsWindow(profile)) {
settings_window_observer_ = std::make_unique<SettingsWindowObserver>();
}
// All profile relevant settings get bound to the current profile.
AttachProfile(profile);
DCHECK_EQ(profile, profile_);
model_->AddObserver(this);
shelf_spinner_controller_ = std::make_unique<ShelfSpinnerController>(this);
// Create either the real window manager or a stub.
MultiUserWindowManagerHelper::CreateInstance();
// On Chrome OS using multi profile we want to switch the content of the shelf
// with a user change. Note that for unit tests the instance can be nullptr.
if (SessionControllerClientImpl::IsMultiProfileAvailable()) {
user_switch_observer_ =
std::make_unique<ChromeShelfControllerUserSwitchObserver>(this);
}
std::unique_ptr<AppServiceAppWindowShelfController> app_service_controller =
std::make_unique<AppServiceAppWindowShelfController>(this);
app_service_app_window_controller_ = app_service_controller.get();
app_window_controllers_.emplace_back(std::move(app_service_controller));
if (web_app::IsWebAppsCrosapiEnabled() &&
apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
DCHECK(proxy);
CHECK(proxy->BrowserAppInstanceRegistry());
browser_app_shelf_controller_ = std::make_unique<BrowserAppShelfController>(
profile, *proxy->BrowserAppInstanceRegistry(), *model_,
*shelf_item_factory_, *shelf_spinner_controller_);
} else {
// Create the browser monitor which will inform the shelf of status changes.
browser_status_monitor_ = std::make_unique<BrowserStatusMonitor>(this);
}
}
ChromeShelfController::~ChromeShelfController() {
// Reset the BrowserStatusMonitor as it has a weak pointer to this.
browser_status_monitor_.reset();
// Reset the app window controllers here since it has a weak pointer to this.
app_service_app_window_controller_ = nullptr;
app_window_controllers_.clear();
// Destroy the ShelfSpinnerController before clearing delegates.
shelf_spinner_controller_.reset();
// Destroy local shelf item delegates; some subclasses have complex cleanup.
model_->DestroyItemDelegates();
model_->RemoveObserver(this);
// Release all profile dependent resources.
ReleaseProfile();
// Get rid of the multi user window manager instance.
MultiUserWindowManagerHelper::DeleteInstance();
if (instance_ == this)
instance_ = nullptr;
}
void ChromeShelfController::Init() {
if (!crosapi::browser_util::IsLacrosPrimaryBrowser()) {
CreateBrowserShortcutItem(/*pinned=*/true);
UpdateBrowserItemState();
}
// Tag all open browser windows with the appropriate shelf id property. This
// associates each window with the shelf item for the active web contents.
for (auto* browser : *BrowserList::GetInstance()) {
if (IsBrowserRepresentedInBrowserList(browser, model_) &&
browser->tab_strip_model()->GetActiveWebContents()) {
SetShelfIDForBrowserWindowContents(
browser, browser->tab_strip_model()->GetActiveWebContents());
}
}
UpdatePinnedAppsFromSync();
if (browser_status_monitor_) {
browser_status_monitor_->Initialize();
}
ReportInitShelfIconList(model_);
}
ash::ShelfID ChromeShelfController::CreateAppItem(
std::unique_ptr<ash::ShelfItemDelegate> item_delegate,
ash::ShelfItemStatus status,
const std::u16string& title) {
return InsertAppItem(std::move(item_delegate), status, model_->item_count(),
ash::TYPE_APP, title);
}
const ash::ShelfItem* ChromeShelfController::GetItem(
const ash::ShelfID& id) const {
return model_->ItemByID(id);
}
void ChromeShelfController::SetItemType(const ash::ShelfID& id,
ash::ShelfItemType type) {
const ash::ShelfItem* item = GetItem(id);
if (item && item->type != type) {
ash::ShelfItem new_item = *item;
new_item.type = type;
model_->Set(model_->ItemIndexByID(id), new_item);
}
}
void ChromeShelfController::SetItemStatus(const ash::ShelfID& id,
ash::ShelfItemStatus status) {
const ash::ShelfItem* item = GetItem(id);
if (item && item->status != status) {
ash::ShelfItem new_item = *item;
new_item.status = status;
model_->Set(model_->ItemIndexByID(id), new_item);
}
}
void ChromeShelfController::SetItemTitle(const ash::ShelfID& id,
const std::u16string& title) {
const ash::ShelfItem* item = GetItem(id);
if (item && item->title != title) {
ash::ShelfItem new_item = *item;
new_item.title = title;
model_->Set(model_->ItemIndexByID(id), new_item);
}
}
void ChromeShelfController::ReplaceWithAppShortcutOrRemove(
const ash::ShelfID& id) {
CHECK(!id.IsNull());
if (IsPinned(id)) {
// Create a new shortcut delegate.
SetItemStatus(id, ash::STATUS_CLOSED);
model_->ReplaceShelfItemDelegate(
id, std::make_unique<AppShortcutShelfItemController>(id));
} else {
RemoveShelfItem(id);
}
}
void ChromeShelfController::UnpinShelfItemInternal(const ash::ShelfID& id) {
const ash::ShelfItem* item = GetItem(id);
if (item && item->status != ash::STATUS_CLOSED)
UnpinRunningAppInternal(model_->ItemIndexByID(id));
else
RemoveShelfItem(id);
}
void ChromeShelfController::SetItemStatusOrRemove(const ash::ShelfID& id,
ash::ShelfItemStatus status) {
if (!IsPinned(id) && status == ash::STATUS_CLOSED)
RemoveShelfItem(id);
else
SetItemStatus(id, status);
}
bool ChromeShelfController::ShouldSyncItemWithReentrancy(
const ash::ShelfItem& item) {
return should_sync_pin_changes_ && ShouldSyncItem(item);
}
bool ChromeShelfController::ShouldSyncItem(const ash::ShelfItem& item) {
return ItemTypeIsPinned(item);
}
bool ChromeShelfController::IsPinned(const ash::ShelfID& id) const {
const ash::ShelfItem* item = GetItem(id);
return item && ItemTypeIsPinned(*item);
}
void ChromeShelfController::SetAppStatus(const std::string& app_id,
ash::ShelfItemStatus status) {
ash::ShelfID id(app_id);
const ash::ShelfItem* item = GetItem(id);
if (item) {
SetItemStatusOrRemove(id, status);
} else if (status != ash::STATUS_CLOSED && !app_id.empty()) {
InsertAppItem(
std::make_unique<AppShortcutShelfItemController>(ash::ShelfID(app_id)),
status, model_->item_count(), ash::TYPE_APP);
}
}
void ChromeShelfController::Close(const ash::ShelfID& id) {
ash::ShelfItemDelegate* delegate = model_->GetShelfItemDelegate(id);
if (!delegate)
return; // May happen if menu closed.
delegate->Close();
}
bool ChromeShelfController::IsOpen(const ash::ShelfID& id) const {
const ash::ShelfItem* item = GetItem(id);
return item && item->status != ash::STATUS_CLOSED;
}
bool ChromeShelfController::IsPlatformApp(const ash::ShelfID& id) {
const extensions::Extension* extension =
GetExtensionForAppID(id.app_id, profile());
// An extension can be synced / updated at any time and therefore not be
// available.
return extension ? extension->is_platform_app() : false;
}
bool ChromeShelfController::UninstallAllowed(const std::string& app_id) {
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(profile())->GetInstalledExtension(
app_id);
const extensions::ManagementPolicy* policy =
extensions::ExtensionSystem::Get(profile())->management_policy();
return extension && policy->UserMayModifySettings(extension, nullptr) &&
!policy->MustRemainInstalled(extension, nullptr);
}
void ChromeShelfController::LaunchApp(const ash::ShelfID& id,
ash::ShelfLaunchSource source,
int event_flags,
int64_t display_id) {
shelf_controller_helper_->LaunchApp(id, source, event_flags, display_id);
}
void ChromeShelfController::SetItemImage(const ash::ShelfID& shelf_id,
const gfx::ImageSkia& image) {
DCHECK(!image.isNull());
if (const auto* item = GetItem(shelf_id)) {
ash::ShelfItem new_item = *item;
new_item.image = image;
new_item.notification_badge_color =
ash::AppIconColorCache::GetInstance().GetLightVibrantColorForApp(
new_item.id.app_id, image);
model_->Set(model_->ItemIndexByID(shelf_id), new_item);
}
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::UpdateItemImage(const std::string& app_id) {
if (auto* icon_loader = GetAppIconLoaderForApp(app_id))
icon_loader->UpdateImage(app_id);
}
void ChromeShelfController::UpdateAppState(content::WebContents* contents,
bool remove) {
ash::ShelfID shelf_id(shelf_controller_helper_->GetAppID(contents));
// If the tab changed apps, remove its association with the previous app item.
if (web_contents_to_app_id_.find(contents) != web_contents_to_app_id_.end()) {
ash::ShelfID old_id(web_contents_to_app_id_[contents]);
if (old_id != shelf_id && GetItem(old_id)) {
// Since GetAppState() will use |web_contents_to_app_id_| we remove
// the connection before calling it.
web_contents_to_app_id_.erase(contents);
SetItemStatusOrRemove(old_id, GetAppState(old_id.app_id));
}
}
if (remove)
web_contents_to_app_id_.erase(contents);
else
web_contents_to_app_id_[contents] = shelf_id.app_id;
SetItemStatusOrRemove(shelf_id, GetAppState(shelf_id.app_id));
}
void ChromeShelfController::UpdateV1AppState(const std::string& app_id) {
for (Browser* browser : *BrowserList::GetInstance()) {
if (!browser->is_type_normal() ||
!multi_user_util::IsProfileFromActiveUser(browser->profile())) {
continue;
}
for (int i = 0; i < browser->tab_strip_model()->count(); ++i) {
content::WebContents* const web_contents =
browser->tab_strip_model()->GetWebContentsAt(i);
if (shelf_controller_helper_->GetAppID(web_contents) != app_id)
continue;
UpdateAppState(web_contents, false /*remove*/);
if (browser->tab_strip_model()->GetActiveWebContents() == web_contents)
SetShelfIDForBrowserWindowContents(browser, web_contents);
}
}
}
std::string ChromeShelfController::GetAppIDForWebContents(
content::WebContents* contents) {
std::string app_id = shelf_controller_helper_->GetAppID(contents);
if (!app_id.empty())
return app_id;
return kChromeAppId;
}
ash::ShelfID ChromeShelfController::GetShelfIDForAppId(
const std::string& app_id) const {
// If there is no dedicated app item, use the browser shortcut item.
const ash::ShelfItem* item =
!app_id.empty() ? GetItem(ash::ShelfID(app_id)) : nullptr;
return item ? item->id : ash::ShelfID(kChromeAppId);
}
ash::ShelfAction ChromeShelfController::ActivateWindowOrMinimizeIfActive(
ui::BaseWindow* window,
bool allow_minimize) {
// We might have to teleport a window back to the current user.
aura::Window* native_window = window->GetNativeWindow();
const AccountId& current_account_id =
multi_user_util::GetAccountIdFromProfile(profile());
if (!MultiUserWindowManagerHelper::GetInstance()->IsWindowOnDesktopOfUser(
native_window, current_account_id)) {
MultiUserWindowManagerHelper::GetWindowManager()->ShowWindowForUser(
native_window, current_account_id);
window->Activate();
return ash::SHELF_ACTION_WINDOW_ACTIVATED;
}
AppListClientImpl* app_list_client = AppListClientImpl::GetInstance();
if (window->IsActive() && allow_minimize &&
!(app_list_client && app_list_client->app_list_target_visibility())) {
window->Minimize();
return ash::SHELF_ACTION_WINDOW_MINIMIZED;
}
window->Show();
window->Activate();
return ash::SHELF_ACTION_WINDOW_ACTIVATED;
}
void ChromeShelfController::ActiveUserChanged(const AccountId& account_id) {
// Store the order of running applications for the user which gets inactive.
RememberUnpinnedRunningApplicationOrder();
// Coming here the default profile is already switched. All profile specific
// resources get released and the new profile gets attached instead.
ReleaseProfile();
// When coming here, the active user has already be changed so that we can
// set it as active.
AttachProfile(ProfileManager::GetActiveUserProfile());
if (browser_status_monitor_) {
// Update the V1 applications.
browser_status_monitor_->ActiveUserChanged(account_id.GetUserEmail());
}
// Save/restore spinners belonging to the old/new user. Must be called before
// notifying the AppWindowControllers, as some of them assume spinners owned
// by the new user have already been added to the shelf.
shelf_spinner_controller_->ActiveUserChanged(account_id);
// Switch the running applications to the new user.
for (auto& controller : app_window_controllers_)
controller->ActiveUserChanged(account_id.GetUserEmail());
// Update the user specific shell properties from the new user profile.
// Shelf preferences are loaded in ChromeShelfController::AttachProfile.
UpdatePinnedAppsFromSync();
// Restore the order of running, but unpinned applications for the activated
// user.
RestoreUnpinnedRunningApplicationOrder(account_id.GetUserEmail());
}
void ChromeShelfController::AdditionalUserAddedToSession(Profile* profile) {
AddAppUpdaterAndIconLoader(profile);
// Switch the running applications to the new user.
for (auto& controller : app_window_controllers_)
controller->AdditionalUserAddedToSession(profile);
}
ash::ShelfItemDelegate::AppMenuItems
ChromeShelfController::GetAppMenuItemsForTesting(const ash::ShelfItem& item) {
ash::ShelfItemDelegate* delegate = model_->GetShelfItemDelegate(item.id);
return delegate ? delegate->GetAppMenuItems(ui::EF_NONE, base::NullCallback())
: ash::ShelfItemDelegate::AppMenuItems();
}
std::vector<aura::Window*> ChromeShelfController::GetArcWindows() {
if (app_service_app_window_controller_)
return app_service_app_window_controller_->GetArcWindows();
return std::vector<aura::Window*>();
}
void ChromeShelfController::ActivateShellApp(const std::string& app_id,
int window_index) {
const ash::ShelfItem* item = GetItem(ash::ShelfID(app_id));
if (item &&
(item->type == ash::TYPE_APP || item->type == ash::TYPE_PINNED_APP)) {
ash::ShelfItemDelegate* delegate = model_->GetShelfItemDelegate(item->id);
AppWindowShelfItemController* item_controller =
delegate->AsAppWindowShelfItemController();
item_controller->ActivateIndexedApp(window_index);
}
}
bool ChromeShelfController::IsWebContentHandledByApplication(
content::WebContents* web_contents,
const std::string& app_id) {
return (web_contents_to_app_id_.find(web_contents) !=
web_contents_to_app_id_.end()) &&
(web_contents_to_app_id_[web_contents] == app_id);
}
gfx::Image ChromeShelfController::GetAppMenuIcon(
content::WebContents* web_contents) const {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
if (!web_contents)
return rb.GetImageNamed(IDR_DEFAULT_FAVICON);
const Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (profile->IsIncognitoProfile())
return rb.GetImageNamed(IDR_ASH_SHELF_LIST_INCOGNITO_BROWSER);
favicon::FaviconDriver* favicon_driver =
favicon::ContentFaviconDriver::FromWebContents(web_contents);
gfx::Image result = favicon_driver->GetFavicon();
if (result.IsEmpty())
return rb.GetImageNamed(IDR_DEFAULT_FAVICON);
return result;
}
std::u16string ChromeShelfController::GetAppMenuTitle(
content::WebContents* web_contents) const {
if (!web_contents)
return l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE);
const std::u16string& title = web_contents->GetTitle();
if (!title.empty())
return title;
WebContentsToAppIDMap::const_iterator iter =
web_contents_to_app_id_.find(web_contents);
if (iter != web_contents_to_app_id_.end()) {
std::string app_id = iter->second;
const extensions::Extension* extension =
GetExtensionForAppID(app_id, profile());
if (extension)
return base::UTF8ToUTF16(extension->name());
}
return l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE);
}
BrowserShortcutShelfItemController*
ChromeShelfController::GetBrowserShortcutShelfItemControllerForTesting() {
ash::ShelfID id(kChromeAppId);
ash::ShelfItemDelegate* delegate = model_->GetShelfItemDelegate(id);
DCHECK(delegate) << "There should be always be a browser shortcut item.";
return static_cast<BrowserShortcutShelfItemController*>(delegate);
}
void ChromeShelfController::UpdateBrowserItemState() {
ash::ShelfItemStatus browser_status = ash::STATUS_CLOSED;
for (auto* browser : *BrowserList::GetInstance()) {
if (IsBrowserRepresentedInBrowserList(browser, model_)) {
browser_status = ash::STATUS_RUNNING;
break;
}
}
if (browser_status == ash::STATUS_CLOSED) {
// If browser shortcut icon is not pinned, remove it.
// Practically, this happens when Lacros is the primary browser.
int item_index =
model_->GetItemIndexForType(ash::TYPE_UNPINNED_BROWSER_SHORTCUT);
if (item_index >= 0) {
model_->RemoveItemAt(item_index);
ReportUpdateShelfIconList(model_);
return;
}
}
const ash::ShelfID chrome_id(kChromeAppId);
if (browser_status == ash::STATUS_RUNNING &&
model_->ItemIndexByID(chrome_id) < 0) {
// If browser short cut is not present, create it.
// This happens iff browser shortcut is not pinned.
CreateBrowserShortcutItem(/*pinned=*/false);
}
int browser_index = model_->ItemIndexByID(chrome_id);
if (browser_index < 0) {
DCHECK_EQ(browser_status, ash::STATUS_CLOSED);
return;
}
ash::ShelfItem browser_item = model_->items()[browser_index];
if (browser_status == browser_item.status) {
// Nothing is changed.
return;
}
browser_item.status = browser_status;
model_->Set(browser_index, browser_item);
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::SetShelfIDForBrowserWindowContents(
Browser* browser,
content::WebContents* web_contents) {
// We need to set the window ShelfID for V1 applications since they are
// content which might change and as such change the application type.
// The browser window may not exist in unit tests.
if (!browser || !browser->window() || !browser->window()->GetNativeWindow() ||
!multi_user_util::IsProfileFromActiveUser(browser->profile())) {
return;
}
const std::string app_id = GetAppIDForWebContents(web_contents);
browser->window()->GetNativeWindow()->SetProperty(ash::kAppIDKey,
new std::string(app_id));
const ash::ShelfID shelf_id = GetShelfIDForAppId(app_id);
browser->window()->GetNativeWindow()->SetProperty(
ash::kShelfIDKey, new std::string(shelf_id.Serialize()));
}
void ChromeShelfController::OnUserProfileReadyToSwitch(Profile* profile) {
if (user_switch_observer_.get())
user_switch_observer_->OnUserProfileReadyToSwitch(profile);
}
ShelfSpinnerController* ChromeShelfController::GetShelfSpinnerController() {
return shelf_spinner_controller_.get();
}
ChromeShelfController::ScopedPinSyncDisabler
ChromeShelfController::GetScopedPinSyncDisabler() {
// Only one temporary disabler should not exist at a time.
DCHECK(should_sync_pin_changes_);
return std::make_unique<base::AutoReset<bool>>(&should_sync_pin_changes_,
false);
}
void ChromeShelfController::SetShelfControllerHelperForTest(
std::unique_ptr<ShelfControllerHelper> helper) {
shelf_controller_helper_ = std::move(helper);
}
void ChromeShelfController::SetAppIconLoadersForTest(
std::vector<std::unique_ptr<AppIconLoader>>& loaders) {
app_icon_loaders_.clear();
for (auto& loader : loaders)
app_icon_loaders_[profile_].push_back(std::move(loader));
}
void ChromeShelfController::SetProfileForTest(Profile* profile) {
profile_ = profile;
latest_active_profile_ = profile;
}
bool ChromeShelfController::AllowedToSetAppPinState(const std::string& app_id,
bool target_pin) const {
return model_->AllowedToSetAppPinState(app_id, target_pin);
}
bool ChromeShelfController::IsAppPinned(const std::string& app_id) {
return model_->IsAppPinned(app_id);
}
void ChromeShelfController::UnpinAppWithID(const std::string& app_id) {
model_->UnpinAppWithID(app_id);
}
void ChromeShelfController::ReplacePinnedItem(const std::string& old_app_id,
const std::string& new_app_id) {
if (!model_->IsAppPinned(old_app_id) || model_->IsAppPinned(new_app_id))
return;
const int index = model_->ItemIndexByAppID(old_app_id);
const ash::ShelfID new_shelf_id(new_app_id);
ash::ShelfItem item;
item.type = ash::TYPE_PINNED_APP;
item.id = new_shelf_id;
// Remove old_app at index and replace with new app.
model_->RemoveItemAt(index);
model_->AddAt(index, item,
std::make_unique<AppShortcutShelfItemController>(item.id));
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::PinAppAtIndex(const std::string& app_id,
int target_index) {
if (target_index < 0 || model_->IsAppPinned(app_id))
return;
const ash::ShelfID new_shelf_id(app_id);
ash::ShelfItem item;
item.type = ash::TYPE_PINNED_APP;
item.id = new_shelf_id;
model_->AddAt(target_index, item,
std::make_unique<AppShortcutShelfItemController>(item.id));
ReportUpdateShelfIconList(model_);
}
int ChromeShelfController::PinnedItemIndexByAppID(const std::string& app_id) {
if (model_->IsAppPinned(app_id)) {
ash::ShelfID shelf_id(app_id);
return model_->ItemIndexByID(shelf_id);
}
return kInvalidIndex;
}
AppIconLoader* ChromeShelfController::GetAppIconLoaderForApp(
const std::string& app_id) {
for (const auto& app_icon_loader :
app_icon_loaders_[latest_active_profile_]) {
if (app_icon_loader->CanLoadImageForApp(app_id))
return app_icon_loader.get();
}
return nullptr;
}
bool ChromeShelfController::CanDoShowAppInfoFlow(
Profile* profile,
const std::string& extension_id) {
return CanShowAppInfoDialog(profile, extension_id);
}
void ChromeShelfController::DoShowAppInfoFlow(Profile* profile,
const std::string& app_id) {
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
auto app_type = proxy->AppRegistryCache().GetAppType(app_id);
// Apps that are not in the App Service may call this function.
// E.g. extensions, apps that are using their platform specific IDs.
if (app_type == apps::AppType::kUnknown) {
return;
}
if (app_type == apps::AppType::kWeb ||
app_type == apps::AppType::kSystemWeb) {
chrome::ShowAppManagementPage(
profile, app_id,
ash::settings::AppManagementEntryPoint::kShelfContextMenuAppInfoWebApp);
} else {
chrome::ShowAppManagementPage(profile,
apps::GetEscapedAppId(app_id, app_type),
ash::settings::AppManagementEntryPoint::
kShelfContextMenuAppInfoChromeApp);
}
}
///////////////////////////////////////////////////////////////////////////////
// ShelfAppUpdater::Delegate:
void ChromeShelfController::OnAppInstalled(
content::BrowserContext* browser_context,
const std::string& app_id) {
if (IsAppPinned(app_id) &&
ShelfControllerHelper::IsAppHiddenFromShelf(profile(), app_id)) {
ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
UnpinShelfItemInternal(ash::ShelfID(app_id));
}
// When the app is pinned to the shelf, or added to the shelf, the app
// probably isn't ready in AppService, so set the title, and load the icon
// again on callback when the app is ready in AppService.
int index = model_->ItemIndexByAppID(app_id);
if (index != kInvalidIndex) {
ash::ShelfItem item = model_->items()[index];
if (item.type == ash::TYPE_APP || item.type == ash::TYPE_PINNED_APP) {
AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(app_id);
if (app_icon_loader) {
app_icon_loader->ClearImage(app_id);
app_icon_loader->FetchImage(app_id);
}
bool needs_update = false;
if (item.title.empty()) {
needs_update = true;
item.title =
ShelfControllerHelper::GetAppTitle(latest_active_profile_, app_id);
}
ash::AppStatus app_status =
ShelfControllerHelper::GetAppStatus(latest_active_profile_, app_id);
if (app_status != item.app_status) {
needs_update = true;
item.app_status = app_status;
}
if (needs_update)
model_->Set(index, item);
}
}
UpdatePinnedAppsFromSync();
}
void ChromeShelfController::OnAppUpdated(
content::BrowserContext* browser_context,
const std::string& app_id,
bool reload_icon) {
// Ensure that icon loader tracks the icon for this app - in particular, this
// is needed when updating ChromeShelfController after user change in
// multi-profile sessions, as icon loaders get reset when clearing the state
// from the previous profile.
int index = model_->ItemIndexByAppID(app_id);
if (index != kInvalidIndex) {
ash::ShelfItem item = model_->items()[index];
if (item.type == ash::TYPE_APP || item.type == ash::TYPE_PINNED_APP) {
if (reload_icon) {
AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(app_id);
if (app_icon_loader)
app_icon_loader->FetchImage(app_id);
}
bool needs_update = false;
ash::AppStatus app_status =
ShelfControllerHelper::GetAppStatus(latest_active_profile_, app_id);
if (app_status != item.app_status) {
needs_update = true;
item.app_status = app_status;
}
std::u16string title =
ShelfControllerHelper::GetAppTitle(latest_active_profile_, app_id);
if (item.title != title) {
needs_update = true;
item.title = title;
}
if (needs_update)
model_->Set(index, item);
}
}
}
void ChromeShelfController::OnAppShowInShelfChanged(
content::BrowserContext* browser_context,
const std::string& app_id,
bool show_in_shelf) {
if (browser_context != profile())
return;
ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
// If the app should be hidden from shelf, make sure it gets unpinned.
if (!show_in_shelf) {
if (IsAppPinned(app_id))
UnpinShelfItemInternal(ash::ShelfID(app_id));
return;
}
// If the app status changed to "shown in shelf", pin the app if user prefs
// (or policy) indicate that the app should be pinned.
const std::vector<ash::ShelfID> pinned_apps =
shelf_prefs_->GetPinnedAppsFromSync(shelf_controller_helper_.get());
// Find the app index within pinned apps.
int index = -1;
for (size_t i = 0; i < pinned_apps.size(); ++i) {
if (pinned_apps[i].app_id == app_id) {
index = i;
break;
}
}
// The app should not be pinned - nothing left to do.
if (index == -1)
return;
// Update apps icon if applicable.
OnAppUpdated(profile(), app_id, /*reload_icon=*/true);
// Calculate the target app index within the model - find the last app in
// `pinned_apps` that precedes `app_id`, and is in shelf model.
int target_index_in_model = 0;
for (int i = index - 1; i >= 0; --i) {
int index_in_model = model_->ItemIndexByID(pinned_apps[i]);
if (index_in_model >= 0 &&
ItemTypeIsPinned(model_->items()[index_in_model])) {
target_index_in_model = index_in_model + 1;
break;
}
}
EnsureAppPinnedInModelAtIndex(app_id, model_->ItemIndexByAppID(app_id),
target_index_in_model);
// Set the pinned by policy flag.
const int final_index = model_->ItemIndexByAppID(app_id);
if (final_index >= 0)
UpdatePinnedByPolicyForItemAtIndex(final_index);
}
void ChromeShelfController::OnAppUninstalledPrepared(
content::BrowserContext* browser_context,
const std::string& app_id,
bool by_migration) {
// Since we might have windowed apps of this type which might have
// outstanding locks which needs to be removed.
const Profile* profile = Profile::FromBrowserContext(browser_context);
ash::ShelfID shelf_id(app_id);
if (GetItem(shelf_id))
CloseWindowedAppsFromRemovedExtension(app_id, profile);
// Some apps may be removed locally. Unpin the item without removing the pin
// position from profile preferences. When needed, it is automatically deleted
// on app list model update.
if (IsAppPinned(app_id) && profile == this->profile()) {
bool show_in_shelf_changed = false;
bool is_app_disabled = false;
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(this->profile());
proxy->AppRegistryCache().ForOneApp(
app_id, [&show_in_shelf_changed,
&is_app_disabled](const apps::AppUpdate& update) {
show_in_shelf_changed = update.ShowInShelfChanged();
is_app_disabled =
update.Readiness() == apps::Readiness::kDisabledByPolicy;
});
// If the app is hidden and disabled, we need to update the app pin state.
// We don't remove the pin position from the preferences, in case we want to
// restore the app pinned state when the app state has changed to blocked or
// enabled.
if (by_migration || (show_in_shelf_changed && is_app_disabled)) {
ScopedPinSyncDisabler scoped_pin_sync_disabler =
GetScopedPinSyncDisabler();
UnpinShelfItemInternal(shelf_id);
} else {
UnpinShelfItemInternal(shelf_id);
}
}
}
///////////////////////////////////////////////////////////////////////////////
// AppIconLoaderDelegate:
void ChromeShelfController::OnAppImageUpdated(const std::string& app_id,
const gfx::ImageSkia& image) {
bool is_standard_icon = true;
if (!AppServiceAppIconLoader::CanLoadImage(latest_active_profile_, app_id))
is_standard_icon = false;
if (is_standard_icon) {
UpdateAppImage(app_id, image);
return;
}
if (!standard_icon_task_runner_) {
standard_icon_task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}
gfx::ImageSkia copy;
if (image.IsThreadSafe()) {
copy = image;
} else {
image.EnsureRepsForSupportedScales();
copy = image.DeepCopy();
}
standard_icon_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&CreateStandardImageOnWorkerThread, copy),
base::BindOnce(&ChromeShelfController::UpdateAppImage,
weak_ptr_factory_.GetWeakPtr(), app_id));
}
void ChromeShelfController::UpdateAppImage(const std::string& app_id,
const gfx::ImageSkia& image) {
// TODO: need to get this working for shortcuts.
for (int index = 0; index < model_->item_count(); ++index) {
ash::ShelfItem item = model_->items()[index];
ash::ShelfItemDelegate* delegate = model_->GetShelfItemDelegate(item.id);
if (!delegate || delegate->image_set_by_controller() ||
item.id.app_id != app_id) {
continue;
}
item.image = image;
shelf_spinner_controller_->MaybeApplySpinningEffect(app_id, &item.image);
item.notification_badge_color =
ash::AppIconColorCache::GetInstance().GetLightVibrantColorForApp(app_id,
image);
model_->Set(index, item);
// It's possible we're waiting on more than one item, so don't break.
}
ReportUpdateShelfIconList(model_);
}
///////////////////////////////////////////////////////////////////////////////
// ChromeShelfController private:
void ChromeShelfController::RememberUnpinnedRunningApplicationOrder() {
RunningAppListIds list;
for (int i = 0; i < model_->item_count(); i++) {
if (model_->items()[i].type == ash::TYPE_APP)
list.push_back(model_->items()[i].id.app_id);
}
const std::string user_email =
multi_user_util::GetAccountIdFromProfile(profile()).GetUserEmail();
last_used_running_application_order_[user_email] = list;
}
void ChromeShelfController::RestoreUnpinnedRunningApplicationOrder(
const std::string& user_id) {
const RunningAppListIdMap::iterator app_id_list =
last_used_running_application_order_.find(user_id);
if (app_id_list == last_used_running_application_order_.end())
return;
// Find the first insertion point for running applications.
int running_index = model_->FirstRunningAppIndex();
for (const std::string& app_id : app_id_list->second) {
const ash::ShelfItem* item = GetItem(ash::ShelfID(app_id));
if (item && item->type == ash::TYPE_APP) {
int app_index = model_->ItemIndexByID(item->id);
DCHECK_GE(app_index, 0);
if (running_index != app_index)
model_->Move(running_index, app_index);
running_index++;
}
}
}
void ChromeShelfController::RemoveShelfItem(const ash::ShelfID& id) {
const int index = model_->ItemIndexByID(id);
if (index >= 0 && index < model_->item_count())
model_->RemoveItemAt(index);
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::PinRunningAppInternal(
int index,
const ash::ShelfID& shelf_id) {
if (GetItem(shelf_id)->type == ash::TYPE_UNPINNED_BROWSER_SHORTCUT) {
// If the item is unpinned browser shortcut, which should never be
// pinned during the session, do nothing.
return;
}
DCHECK_EQ(GetItem(shelf_id)->type, ash::TYPE_APP);
SetItemType(shelf_id, ash::TYPE_PINNED_APP);
int running_index = model_->ItemIndexByID(shelf_id);
if (running_index < index)
--index;
if (running_index != index)
model_->Move(running_index, index);
}
void ChromeShelfController::UnpinRunningAppInternal(int index) {
DCHECK(index >= 0 && index < model_->item_count());
const ash::ShelfItem& item = model_->items()[index];
DCHECK_EQ(item.type, ash::TYPE_PINNED_APP);
SetItemType(item.id, ash::TYPE_APP);
}
void ChromeShelfController::SyncPinPosition(const ash::ShelfID& shelf_id) {
DCHECK(should_sync_pin_changes_);
DCHECK(!shelf_id.IsNull());
const int max_index = model_->item_count();
const int index = model_->ItemIndexByID(shelf_id);
DCHECK_GE(index, 0);
ash::ShelfID shelf_id_before;
std::vector<ash::ShelfID> shelf_ids_after;
for (int i = index - 1; i >= 0; --i) {
if (ShouldSyncItem(model_->items()[i])) {
shelf_id_before = model_->items()[i].id;
break;
}
}
for (int i = index + 1; i < max_index; ++i) {
const ash::ShelfID& shelf_id_after = model_->items()[i].id;
if (ShouldSyncItem(model_->items()[i]))
shelf_ids_after.push_back(shelf_id_after);
}
shelf_prefs_->SetPinPosition(shelf_id, shelf_id_before, shelf_ids_after);
}
void ChromeShelfController::OnSyncModelUpdated() {
ScheduleUpdatePinnedAppsFromSync();
}
void ChromeShelfController::OnIsSyncingChanged() {
UpdatePinnedAppsFromSync();
InitLocalShelfPrefsIfOsPrefsAreSyncing();
}
void ChromeShelfController::InitLocalShelfPrefsIfOsPrefsAreSyncing() {
// Wait until the initial sync happens.
auto* pref_service = PrefServiceSyncableFromProfile(profile());
bool is_syncing = pref_service->AreOsPrefsSyncing();
if (!is_syncing)
return;
// Initialize the local prefs if this is the first time sync has occurred.
shelf_prefs_->InitLocalPref(profile()->GetPrefs(),
ash::prefs::kShelfAlignmentLocal,
ash::prefs::kShelfAlignment);
shelf_prefs_->InitLocalPref(profile()->GetPrefs(),
ash::prefs::kShelfAutoHideBehaviorLocal,
ash::prefs::kShelfAutoHideBehavior);
}
void ChromeShelfController::ScheduleUpdatePinnedAppsFromSync() {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ChromeShelfController::UpdatePinnedAppsFromSync,
weak_ptr_factory_.GetWeakPtr()));
}
void ChromeShelfController::UpdatePinnedAppsFromSync() {
// Do not sync pin changes during this function to avoid cyclical updates.
// This function makes the shelf model reflect synced prefs, and should not
// cyclically trigger sync changes (eg. ShelfItemAdded calls SyncPinPosition).
ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
const std::vector<ash::ShelfID> pinned_apps =
shelf_prefs_->GetPinnedAppsFromSync(shelf_controller_helper_.get());
int index = 0;
// Apply pins in two steps. At the first step, go through the list of apps to
// pin, move existing pin to current position specified by |index| or create
// the new pin at that position.
for (const auto& pref_shelf_id : pinned_apps) {
const std::string app_id = pref_shelf_id.app_id;
// Do not show apps in the shelf if they are explicitly forbidden.
if (ShelfControllerHelper::IsAppHiddenFromShelf(profile(), app_id))
continue;
// Update apps icon if applicable.
OnAppUpdated(profile(), app_id, /*reload_icon=*/true);
// Find existing pin or app from the right of current |index|.
int app_index = index;
for (; app_index < model_->item_count(); ++app_index) {
const ash::ShelfItem& item = model_->items()[app_index];
if (item.id == pref_shelf_id)
break;
}
const bool item_pinned = EnsureAppPinnedInModelAtIndex(
app_id,
/*current_index=*/app_index < model_->item_count() ? app_index : -1,
/*target_index=*/index);
if (item_pinned)
++index;
}
// At second step remove any pin to the right from the current index.
while (index < model_->item_count()) {
const ash::ShelfItem& item = model_->items()[index];
if (item.type == ash::TYPE_PINNED_APP)
UnpinShelfItemInternal(item.id);
else
++index;
}
UpdateAppsPinStatesFromPrefs();
ReportUpdateShelfIconList(model_);
}
bool ChromeShelfController::EnsureAppPinnedInModelAtIndex(
const std::string& app_id,
int current_index,
int target_index) {
// Passing current app index in model as an argument is an optimization in
// case this method is used while looping over items that avoids extra pass
// over items in the model to find the app index.
DCHECK_EQ(current_index, model_->ItemIndexByAppID(app_id));
if (current_index >= 0) {
const ash::ShelfItem item = model_->items()[current_index];
if (ItemTypeIsPinned(item)) {
model_->Move(current_index, target_index);
} else {
PinRunningAppInternal(target_index, item.id);
}
DCHECK_EQ(model_->ItemIndexByID(item.id), target_index);
return true;
}
// app_id may be kChromeAppId. This happens when sync happens,
// but Lacros becomes the primary browser so that the browser
// shortcut is unpinned. Do nothing then.
if (app_id == kChromeAppId)
return false;
// We need to create a new pin for a synced app.
ash::ShelfItem item;
std::unique_ptr<ash::ShelfItemDelegate> item_delegate;
if (!shelf_item_factory()->CreateShelfItemForAppId(app_id, &item,
&item_delegate)) {
return false;
}
InsertAppItem(std::move(item_delegate), ash::STATUS_CLOSED, target_index,
ash::TYPE_PINNED_APP,
/*title=*/std::u16string());
return true;
}
void ChromeShelfController::UpdateAppsPinStatesFromPrefs() {
for (int index = 0; index < model_->item_count(); index++) {
UpdatePinnedByPolicyForItemAtIndex(index);
}
}
void ChromeShelfController::UpdatePinnedByPolicyForItemAtIndex(
int model_index) {
ash::ShelfItem item = model_->items()[model_index];
const bool pinned_by_policy =
GetPinnableForAppID(item.id.app_id, profile()) ==
AppListControllerDelegate::PIN_FIXED;
if (item.pinned_by_policy != pinned_by_policy) {
item.pinned_by_policy = pinned_by_policy;
model_->Set(model_index, item);
}
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::UpdateForcedPinStateForItemAtIndex(
int model_index) {
ash::ShelfItem item = model_->items()[model_index];
auto app_type = apps::AppServiceProxyFactory::GetForProfile(profile())
->AppRegistryCache()
.GetAppType(item.id.app_id);
const bool pin_state_forced_by_type =
!IsAppPinEditable(app_type, item.id.app_id, profile());
if (item.pin_state_forced_by_type != pin_state_forced_by_type) {
item.pin_state_forced_by_type = pin_state_forced_by_type;
model_->Set(model_index, item);
}
ReportUpdateShelfIconList(model_);
}
ash::ShelfItemStatus ChromeShelfController::GetAppState(
const std::string& app_id) {
for (auto& it : web_contents_to_app_id_) {
if (it.second == app_id) {
Browser* browser = chrome::FindBrowserWithWebContents(it.first);
// Usually there should never be an item in our |web_contents_to_app_id_|
// list which got deleted already. However - in some situations e.g.
// Browser::SwapTabContent there is temporarily no associated browser.
// TODO(jamescook): This test may not be necessary anymore.
if (!browser)
continue;
return ash::STATUS_RUNNING;
}
}
return ash::STATUS_CLOSED;
}
ash::ShelfID ChromeShelfController::InsertAppItem(
std::unique_ptr<ash::ShelfItemDelegate> item_delegate,
ash::ShelfItemStatus status,
int index,
ash::ShelfItemType shelf_item_type,
const std::u16string& title) {
CHECK(item_delegate);
if (GetItem(item_delegate->shelf_id())) {
static bool once = true;
if (once) {
base::debug::DumpWithoutCrashing();
once = false;
}
return item_delegate->shelf_id();
}
ash::ShelfItem item;
item.status = status;
item.type = shelf_item_type;
item.id = item_delegate->shelf_id();
item.title = title;
item.app_status = ShelfControllerHelper::GetAppStatus(
latest_active_profile_, item_delegate->shelf_id().app_id);
model_->AddAt(index, item, std::move(item_delegate));
ReportUpdateShelfIconList(model_);
return item.id;
}
void ChromeShelfController::CreateBrowserShortcutItem(bool pinned) {
// Do not sync the pin position of the browser shortcut item yet; its initial
// position before prefs have loaded is unimportant and the sync service may
// not yet be initialized.
ScopedPinSyncDisabler scoped_pin_sync_disabler = GetScopedPinSyncDisabler();
ash::ShelfItem browser_shortcut;
browser_shortcut.type =
pinned ? ash::TYPE_BROWSER_SHORTCUT : ash::TYPE_UNPINNED_BROWSER_SHORTCUT;
browser_shortcut.id = ash::ShelfID(kChromeAppId);
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
browser_shortcut.image = *rb.GetImageSkiaNamed(IDR_CHROME_APP_ICON_192);
browser_shortcut.notification_badge_color =
ash::AppIconColorCache::GetInstance().GetLightVibrantColorForApp(
kChromeAppId, browser_shortcut.image);
browser_shortcut.title = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME);
// If pinned, add the item towards the start of the shelf, it will be ordered
// by weight. Otherwise put at the end as usual.
if (pinned) {
model_->AddAt(0, browser_shortcut,
std::make_unique<BrowserShortcutShelfItemController>(model_));
} else {
model_->Add(browser_shortcut,
std::make_unique<BrowserShortcutShelfItemController>(model_));
}
ReportUpdateShelfIconList(model_);
}
int ChromeShelfController::FindInsertionPoint() {
for (int i = model_->item_count() - 1; i > 0; --i) {
if (ItemTypeIsPinned(model_->items()[i]))
return i;
}
return 0;
}
void ChromeShelfController::CloseWindowedAppsFromRemovedExtension(
const std::string& app_id,
const Profile* profile) {
// This function cannot rely on the controller's enumeration functionality
// since the extension has already been unloaded.
std::vector<Browser*> browser_to_close;
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
if ((browser->is_type_app() || browser->is_type_app_popup()) &&
app_id == web_app::GetAppIdFromApplicationName(browser->app_name()) &&
profile == browser->profile()) {
browser_to_close.push_back(browser);
}
}
while (!browser_to_close.empty()) {
TabStripModel* tab_strip = browser_to_close.back()->tab_strip_model();
if (!tab_strip->empty())
tab_strip->CloseWebContentsAt(0, TabCloseTypes::CLOSE_NONE);
browser_to_close.pop_back();
}
}
void ChromeShelfController::AddAppUpdaterAndIconLoader(Profile* profile) {
latest_active_profile_ = ProfileManager::GetActiveUserProfile();
// For chrome restart, additional users are added during the system
// startup phase, but we should not run the switch user process.
if (profile == latest_active_profile_) {
// Either add the profile to the list of known profiles and make it the
// active one for some functions of ShelfControllerHelper or create a new
// one.
if (!shelf_controller_helper_.get()) {
shelf_controller_helper_ =
std::make_unique<ShelfControllerHelper>(profile);
} else {
shelf_controller_helper_->set_profile(profile);
}
}
if (!base::Contains(app_updaters_, profile)) {
std::unique_ptr<ShelfAppUpdater> app_service_app_updater(
new ShelfAppServiceAppUpdater(this, profile));
app_updaters_[profile].push_back(std::move(app_service_app_updater));
// Some special extensions open new windows, and on Chrome OS, those windows
// should show the extension icon in the shelf. Extensions are not present
// in the App Service, so use ShelfExtensionAppUpdater to handle
// extensions life-cycle events.
std::unique_ptr<ShelfExtensionAppUpdater> extension_app_updater(
new ShelfExtensionAppUpdater(this, profile,
/*extensions_only=*/true));
app_updaters_[profile].push_back(std::move(extension_app_updater));
}
if (!base::Contains(app_icon_loaders_, profile)) {
std::unique_ptr<AppIconLoader> app_service_app_icon_loader =
std::make_unique<AppServiceAppIconLoader>(
profile, extension_misc::EXTENSION_ICON_MEDIUM, this);
app_icon_loaders_[profile].push_back(
std::move(app_service_app_icon_loader));
// Some special extensions open new windows, and on Chrome OS, those windows
// should show the extension icon in the shelf. Extensions are not present
// in the App Service, so try loading extensions icon using
// ChromeAppIconLoader.
std::unique_ptr<extensions::ChromeAppIconLoader> chrome_app_icon_loader =
std::make_unique<extensions::ChromeAppIconLoader>(
profile, extension_misc::EXTENSION_ICON_MEDIUM,
base::BindRepeating(&app_list::MaybeResizeAndPadIconForMd), this);
chrome_app_icon_loader->SetExtensionsOnly();
app_icon_loaders_[profile].push_back(std::move(chrome_app_icon_loader));
}
}
void ChromeShelfController::AttachProfile(Profile* profile_to_attach) {
profile_ = profile_to_attach;
latest_active_profile_ = profile_to_attach;
AddAppUpdaterAndIconLoader(profile_to_attach);
pref_change_registrar_.Init(profile()->GetPrefs());
pref_change_registrar_.Add(
prefs::kPolicyPinnedLauncherApps,
base::BindRepeating(&ChromeShelfController::UpdatePinnedAppsFromSync,
base::Unretained(this)));
pref_change_registrar_.Add(
arc::prefs::kArcEnabled,
base::BindRepeating(&ChromeShelfController::UpdatePinnedAppsFromSync,
base::Unretained(this)));
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile());
if (app_list_syncable_service)
app_list_syncable_service->AddObserverAndStart(this);
PrefServiceSyncableFromProfile(profile())->AddObserver(this);
InitLocalShelfPrefsIfOsPrefsAreSyncing();
shelf_prefs_->AttachProfile(profile_to_attach);
}
void ChromeShelfController::ReleaseProfile() {
pref_change_registrar_.RemoveAll();
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile());
if (app_list_syncable_service)
app_list_syncable_service->RemoveObserver(this);
PrefServiceSyncableFromProfile(profile())->RemoveObserver(this);
}
///////////////////////////////////////////////////////////////////////////////
// ash::ShelfModelObserver:
void ChromeShelfController::ShelfItemAdded(int index) {
ash::ShelfID id = model_->items()[index].id;
// Fetch the app icon, this may synchronously update the item's image.
AppIconLoader* app_icon_loader = GetAppIconLoaderForApp(id.app_id);
if (app_icon_loader)
app_icon_loader->FetchImage(id.app_id);
// Update the item with any other missing Chrome-specific info.
// Construct |item| after FetchImage, which might synchronously load an image.
ash::ShelfItem item = model_->items()[index];
if (item.type == ash::TYPE_APP || item.type == ash::TYPE_PINNED_APP) {
bool needs_update = false;
if (item.title.empty()) {
needs_update = true;
item.title =
ShelfControllerHelper::GetAppTitle(latest_active_profile_, id.app_id);
}
if (!BrowserAppShelfControllerShouldHandleApp(id.app_id,
latest_active_profile_)) {
ash::ShelfItemStatus status = GetAppState(id.app_id);
if (status != item.status && status != ash::STATUS_CLOSED) {
needs_update = true;
item.status = status;
}
}
ash::AppStatus app_status =
ShelfControllerHelper::GetAppStatus(latest_active_profile_, id.app_id);
if (app_status != item.app_status) {
needs_update = true;
item.app_status = app_status;
}
if (needs_update)
model_->Set(index, item);
}
UpdateForcedPinStateForItemAtIndex(index);
// Update the pin position preference as needed.
if (ShouldSyncItemWithReentrancy(item))
SyncPinPosition(item.id);
ReportUpdateShelfIconList(model_);
}
void ChromeShelfController::ShelfItemRemoved(int index,
const ash::ShelfItem& old_item) {
// Remove the pin position from preferences as needed.
if (ShouldSyncItemWithReentrancy(old_item))
shelf_prefs_->RemovePinPosition(profile(), old_item.id);
if (auto* app_icon_loader = GetAppIconLoaderForApp(old_item.id.app_id))
app_icon_loader->ClearImage(old_item.id.app_id);
}
void ChromeShelfController::ShelfItemMoved(int start_index, int target_index) {
// Update the pin position preference as needed.
const ash::ShelfItem& item = model_->items()[target_index];
if (ShouldSyncItemWithReentrancy(item))
SyncPinPosition(item.id);
}
void ChromeShelfController::ShelfItemChanged(int index,
const ash::ShelfItem& old_item) {
// Add or remove the pin position from preferences as needed.
const ash::ShelfItem& item = model_->items()[index];
if (!ItemTypeIsPinned(old_item) && ShouldSyncItemWithReentrancy(item))
SyncPinPosition(item.id);
else if (ShouldSyncItemWithReentrancy(old_item) && !ItemTypeIsPinned(item))
shelf_prefs_->RemovePinPosition(profile(), old_item.id);
ReportUpdateShelfIconList(model_);
}