| // Copyright 2012 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_prefs.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <ostream> |
| #include <set> |
| #include <utility> |
| |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/constants/ash_switches.h" |
| #include "ash/constants/web_app_id_constants.h" |
| #include "ash/public/cpp/shelf_types.h" |
| #include "ash/webui/mall/app_id.h" |
| #include "ash/webui/projector_app/public/cpp/projector_app_constants.h" |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/extend.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/values.h" |
| #include "chrome/browser/apps/app_preload_service/app_preload_service.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/package_id_util.h" |
| #include "chrome/browser/apps/app_service/policy_util.h" |
| #include "chrome/browser/ash/app_list/app_list_syncable_service.h" |
| #include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h" |
| #include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h" |
| #include "chrome/browser/ash/file_manager/prefs_migration_uma.h" |
| #include "chrome/browser/ash/login/demo_mode/demo_session.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/prefs/pref_service_syncable_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/ui/ash/shelf/shelf_controller_helper.h" |
| #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/ash/components/default_pinned_apps/default_pinned_apps.h" |
| #include "chromeos/ash/components/file_manager/app_id.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "components/app_constants/constants.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_registry.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/sync/base/user_selectable_type.h" |
| #include "components/sync/model/string_ordinal.h" |
| #include "components/sync/service/sync_service.h" |
| #include "components/sync/service/sync_user_settings.h" |
| #include "components/sync_preferences/pref_service_syncable.h" |
| #include "extensions/common/constants.h" |
| |
| namespace { |
| |
| // Returns a result after `lhs` and before `rhs` if they are valid, else returns |
| // initial-ordinal. |
| syncer::StringOrdinal CreateBetween(const syncer::StringOrdinal& lhs, |
| const syncer::StringOrdinal& rhs) { |
| if (lhs.IsValid() && rhs.IsValid()) { |
| return lhs.CreateBetween(rhs); |
| } |
| if (lhs.IsValid()) { |
| return lhs.CreateAfter(); |
| } |
| if (rhs.IsValid()) { |
| return rhs.CreateBefore(); |
| } |
| return syncer::StringOrdinal::CreateInitialOrdinal(); |
| } |
| |
| struct PositionItemId { |
| syncer::StringOrdinal position; |
| std::string item_id; |
| }; |
| |
| // Template for GetNextPositionItemIdAfter() and GetNextPositionBefore(). |
| // Returns the adjacent pin (before or after based on `compare` to `position`. |
| // If |exclude_chrome| is true then Chrome app is not processed. Returns invalid |
| // if `position` is not found or has no adjacent item. |
| template <typename Compare> |
| PositionItemId GetAdjacentPosition( |
| app_list::AppListSyncableService* syncable_service, |
| const syncer::StringOrdinal& position, |
| bool exclude_chrome, |
| Compare compare) { |
| PositionItemId result; |
| for (const auto& [item_id, sync_item] : syncable_service->sync_items()) { |
| if (!sync_item->item_pin_ordinal.IsValid()) { |
| continue; |
| } |
| if (exclude_chrome && item_id == app_constants::kChromeAppId) { |
| continue; |
| } |
| if (position.IsValid() && !compare(position, sync_item->item_pin_ordinal)) { |
| continue; |
| } |
| |
| if (!result.position.IsValid() || |
| compare(sync_item->item_pin_ordinal, result.position)) { |
| result.position = sync_item->item_pin_ordinal; |
| result.item_id = item_id; |
| } |
| } |
| return result; |
| } |
| |
| // Returns the next pin position and item ID after `position`. |
| PositionItemId GetNextPositionItemIdAfter( |
| app_list::AppListSyncableService* syncable_service, |
| const syncer::StringOrdinal& position, |
| bool exclude_chrome = false) { |
| return GetAdjacentPosition( |
| syncable_service, position, exclude_chrome, |
| [](const syncer::StringOrdinal& a, const syncer::StringOrdinal& b) { |
| return a.LessThan(b); |
| }); |
| } |
| |
| // Returns the next pin position before `position`. |
| syncer::StringOrdinal GetNextPositionBefore( |
| app_list::AppListSyncableService* syncable_service, |
| const syncer::StringOrdinal& position, |
| bool exclude_chrome = false) { |
| return GetAdjacentPosition( |
| syncable_service, position, exclude_chrome, |
| [](const syncer::StringOrdinal& a, |
| const syncer::StringOrdinal& b) { return a.GreaterThan(b); }) |
| .position; |
| } |
| |
| // Returns the last pin position. |
| syncer::StringOrdinal GetLastPosition( |
| app_list::AppListSyncableService* syncable_service) { |
| syncer::StringOrdinal invalid; |
| return GetNextPositionBefore(syncable_service, invalid); |
| } |
| |
| // Returns pinned app position even if app is not currently visible on device |
| // that is leftmost item on the shelf. If |exclude_chrome| is true then Chrome |
| // app is not processed. if nothing pinned found, returns an invalid ordinal. |
| syncer::StringOrdinal GetFirstPinnedAppPosition( |
| app_list::AppListSyncableService* syncable_service, |
| bool exclude_chrome) { |
| syncer::StringOrdinal invalid; |
| return GetNextPositionItemIdAfter(syncable_service, invalid, exclude_chrome) |
| .position; |
| } |
| |
| // Helper to create pin position that stays before any synced app, even if |
| // app is not currently visible on a device. |
| syncer::StringOrdinal CreateFirstPinPosition( |
| app_list::AppListSyncableService* syncable_service) { |
| const syncer::StringOrdinal position = |
| GetFirstPinnedAppPosition(syncable_service, false /* exclude_chrome */); |
| return position.IsValid() ? position.CreateBefore() |
| : syncer::StringOrdinal::CreateInitialOrdinal(); |
| } |
| |
| // Ensures |app_id| is pinned. If it is not pinned, makes it pinned in the first |
| // position. |
| void EnsurePinnedOrMakeFirst( |
| const std::string& app_id, |
| app_list::AppListSyncableService* syncable_service) { |
| // This piece prevents accidental side-effects to the SetPinPosition() call |
| // below. |
| CHECK_EQ(app_id, app_constants::kChromeAppId); |
| syncer::StringOrdinal position = syncable_service->GetPinPosition(app_id); |
| if (!position.IsValid()) { |
| position = CreateFirstPinPosition(syncable_service); |
| syncable_service->SetPinPosition(app_id, position); |
| } |
| } |
| |
| // Returns pin position of app matching `package_id`. |
| syncer::StringOrdinal GetAppPosition( |
| Profile* profile, |
| apps::PackageId package_id, |
| app_list::AppListSyncableService* syncable_service) { |
| std::optional<std::string> app_id = |
| apps_util::GetAppWithPackageId(profile, package_id); |
| if (!app_id) { |
| return syncer::StringOrdinal(); |
| } |
| return syncable_service->GetPinPosition(*app_id); |
| } |
| |
| constexpr char kDefaultPinnedAppsKey[] = "default"; |
| constexpr char kPreloadPinnedAppsKey[] = "preload"; |
| |
| bool should_add_default_apps_for_test = false; |
| |
| struct PinInfo { |
| PinInfo(const std::string& app_id, const syncer::StringOrdinal& item_ordinal) |
| : app_id(app_id), item_ordinal(item_ordinal) {} |
| |
| std::string app_id; |
| syncer::StringOrdinal item_ordinal; |
| }; |
| |
| // Helper function that returns the right pref string based on device type. |
| // This is required because tablet form factor devices do not sync app |
| // positions and pin preferences. |
| std::string GetShelfDefaultPinLayoutPref() { |
| if (ash::switches::IsTabletFormFactor()) { |
| return prefs::kShelfDefaultPinLayoutRollsForTabletFormFactor; |
| } |
| |
| return prefs::kShelfDefaultPinLayoutRolls; |
| } |
| |
| // Returns true in case default pin layout configuration could be applied |
| // safely. That means all required components are synced or worked in local |
| // mode. |
| bool IsSafeToApplyDefaultPinLayout(Profile* profile) { |
| const syncer::SyncService* sync_service = |
| SyncServiceFactory::GetForProfile(profile); |
| // No |sync_service| in incognito mode. |
| if (!sync_service) { |
| return true; |
| } |
| |
| // Tablet form-factor devices do not have position sync. |
| if (ash::switches::IsTabletFormFactor()) { |
| return true; |
| } |
| |
| // Some browser tests don't start sync fully as there is no server to download |
| // the initial data from. This prevents applying the default pin layout, |
| // required in some tests. To support this, the behavior can be overridden via |
| // command-line flags. |
| if (ash::switches::ShouldAllowDefaultShelfPinLayoutIgnoringSync()) { |
| return true; |
| } |
| |
| const syncer::SyncUserSettings* settings = sync_service->GetUserSettings(); |
| |
| // If App sync is not yet started, don't apply default pin apps once synced |
| // apps is likely override it. There is a case when App sync is disabled and |
| // in last case local cache is available immediately. |
| if (sync_service->IsSyncFeatureEnabled() && |
| settings->GetSelectedOsTypes().Has( |
| syncer::UserSelectableOsType::kOsApps) && |
| !app_list::AppListSyncableServiceFactory::GetForProfile(profile) |
| ->IsSyncing()) { |
| return false; |
| } |
| |
| // If shelf pin layout rolls preference is not started yet then we cannot say |
| // if we rolled layout or not. |
| if (sync_service->IsSyncFeatureEnabled() && |
| settings->GetSelectedOsTypes().Has( |
| syncer::UserSelectableOsType::kOsPreferences) && |
| !PrefServiceSyncableFromProfile(profile)->AreOsPrefsSyncing()) { |
| return false; |
| } |
| return true; |
| } |
| |
| // Helper to create and insert pins on the shelf for the set of apps defined in |
| // |app_ids| after Chrome in the first position and before any other pinned app. |
| // If Chrome is not the first pinned app then apps are pinned before any other |
| // app. |
| void InsertPinsAfterChromeAndBeforeFirstPinnedApp( |
| app_list::AppListSyncableService* syncable_service, |
| base::span<const std::string> app_ids) { |
| // Chrome must be pinned at this point. |
| syncer::StringOrdinal chrome_position = |
| syncable_service->GetPinPosition(app_constants::kChromeAppId); |
| DCHECK(chrome_position.IsValid()); |
| |
| // New pins are inserted after this position. |
| syncer::StringOrdinal after; |
| // New pins are inserted before this position. |
| syncer::StringOrdinal before = |
| GetFirstPinnedAppPosition(syncable_service, /*exclude_chrome=*/true); |
| |
| if (!before.IsValid()) { |
| before = chrome_position.CreateAfter(); |
| } |
| |
| if (before.GreaterThan(chrome_position)) { |
| // Perfect case, Chrome is the first pinned app and we have next pinned app. |
| after = chrome_position; |
| } else { |
| after = before.CreateBefore(); |
| } |
| |
| for (const auto& app_id : app_ids) { |
| // Check if we already processed the current app. |
| auto* sync_item = syncable_service->GetSyncItem(app_id); |
| if (sync_item && sync_item->item_pin_ordinal.IsValid()) { |
| continue; |
| } |
| const syncer::StringOrdinal position = after.CreateBetween(before); |
| syncable_service->SetPinPosition(app_id, position); |
| |
| // Shift after position, next policy pin position will be created after |
| // current item. |
| after = position; |
| } |
| } |
| |
| void AddGeminiAppPinIfNeeded( |
| Profile* profile, |
| ShelfControllerHelper* helper, |
| app_list::AppListSyncableService* syncable_service) { |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| if (!chromeos::features::IsGeminiAppPreinstallEnabled()) { |
| return; |
| } |
| |
| if (!profile->GetPrefs()->GetList(prefs::kShelfGeminiAppPinRolls).empty()) { |
| return; |
| } |
| |
| if (!helper->IsAppDefaultInstalled(profile, ash::kGeminiAppId)) { |
| return; |
| } |
| |
| const app_list::AppListSyncableService::SyncItem* sync_item = |
| syncable_service->GetSyncItem(ash::kGeminiAppId); |
| if (sync_item && sync_item->item_pin_ordinal.IsValid()) { |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| prefs::kShelfGeminiAppPinRolls); |
| update->Append("v1"); |
| return; |
| } |
| |
| // Pin the Gemini app before Chrome. |
| syncable_service->SetPinPosition(ash::kGeminiAppId, |
| CreateFirstPinPosition(syncable_service)); |
| { |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| prefs::kShelfGeminiAppPinRolls); |
| update->Append("v1"); |
| } |
| #endif // GOOGLE_CHROME_BRANDING |
| } |
| |
| // Pins app_id to the shelf after Chrome and after any apps in skip_app_ids that |
| // appear immediately after Chrome. |
| // Leaves the app where it is if it is already pinned. |
| void PinAfterChromeIfNotPresent(app_list::AppListSyncableService* syncable_service, |
| const std::vector<std::string>& skip_app_ids, |
| const std::string& app_id) { |
| // If already pinned, do nothing. |
| const app_list::AppListSyncableService::SyncItem* existing_sync_item = |
| syncable_service->GetSyncItem(app_id); |
| if (existing_sync_item && existing_sync_item->item_pin_ordinal.IsValid()) { |
| return; |
| } |
| |
| syncer::StringOrdinal current_position = |
| syncable_service->GetPinPosition(app_constants::kChromeAppId); |
| CHECK(current_position.IsValid()); |
| syncer::StringOrdinal next_position; |
| |
| // Increment current position until either the end of the shelf is reached or |
| // a non-skip app is found. |
| for (size_t i = 0; i < skip_app_ids.size(); ++i) { |
| PositionItemId next = |
| GetNextPositionItemIdAfter(syncable_service, current_position); |
| if (!next.position.IsValid() || |
| !base::Contains(skip_app_ids, next.item_id)) { |
| next_position = next.position; |
| break; |
| } |
| current_position = next.position; |
| next_position = syncer::StringOrdinal(); |
| } |
| |
| if (!next_position.IsValid()) { |
| next_position = |
| GetNextPositionItemIdAfter(syncable_service, current_position).position; |
| } |
| |
| syncable_service->SetPinPosition( |
| app_id, CreateBetween(current_position, next_position)); |
| } |
| |
| // Ensures the NotebookLM app is pinned to the shelf after Chrome and Gemini, |
| // when NotebookLM pinning is enabled. |
| void AddNotebookLmAppPinIfNeeded( |
| Profile* profile, |
| app_list::AppListSyncableService* syncable_service) { |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| // Allow manual testers to reset the sync state easily. |
| if (base::FeatureList::IsEnabled( |
| chromeos::features::kNotebookLmAppShelfPinReset)) { |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| prefs::kShelfNotebookLmAppPinRolls); |
| |
| update->clear(); |
| return; |
| } |
| |
| if (!base::FeatureList::IsEnabled( |
| chromeos::features::kNotebookLmAppShelfPin) || |
| !ShelfControllerHelper::IsAppDefaultInstalled(profile, |
| ash::kNotebookLmAppId) || |
| !profile->GetPrefs() |
| ->GetList(prefs::kShelfNotebookLmAppPinRolls) |
| .empty()) { |
| return; |
| } |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| prefs::kShelfNotebookLmAppPinRolls); |
| update->Append("v1"); |
| |
| PinAfterChromeIfNotPresent(syncable_service, {ash::kGeminiAppId}, |
| ash::kNotebookLmAppId); |
| #endif // GOOGLE_CHROME_BRANDING |
| } |
| |
| // Ensures the Mall app is pinned to the shelf after Chrome, Gemini and |
| // NotebookLM, when Mall is enabled. |
| void AddMallPinIfNeeded(Profile* profile, |
| app_list::AppListSyncableService* syncable_service) { |
| // When Mall SWA is enabled, pin the Mall SWA once, and use a synced pref to |
| // make sure it doesn't pin a second time. Users have the option to unpin the |
| // SWA. |
| if (!profile->GetPrefs()->GetList(prefs::kShelfMallAppPinRolls).empty()) { |
| return; |
| } |
| |
| if (!ShelfControllerHelper::IsAppDefaultInstalled(profile, |
| ash::kMallSystemAppId)) { |
| return; |
| } |
| |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| prefs::kShelfMallAppPinRolls); |
| update->Append("v1"); |
| |
| std::vector<std::string> skip_app_ids = {ash::kGeminiAppId}; |
| if (base::FeatureList::IsEnabled( |
| chromeos::features::kNotebookLmAppShelfPin)) { |
| skip_app_ids.push_back(ash::kNotebookLmAppId); |
| } |
| PinAfterChromeIfNotPresent(syncable_service, skip_app_ids, ash::kMallSystemAppId); |
| } |
| |
| void SetPreloadPinComplete(Profile* profile) { |
| ScopedListPrefUpdate update(profile->GetPrefs(), |
| GetShelfDefaultPinLayoutPref()); |
| update->Append(kPreloadPinnedAppsKey); |
| } |
| |
| } // namespace |
| |
| ChromeShelfPrefs::ChromeShelfPrefs(Profile* profile) : profile_(profile) {} |
| |
| ChromeShelfPrefs::~ChromeShelfPrefs() = default; |
| |
| void ChromeShelfPrefs::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterListPref(prefs::kPolicyPinnedLauncherApps); |
| registry->RegisterListPref( |
| prefs::kShelfDefaultPinLayoutRolls, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PRIORITY_PREF); |
| registry->RegisterListPref( |
| prefs::kShelfGeminiAppPinRolls, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF); |
| registry->RegisterListPref( |
| prefs::kShelfNotebookLmAppPinRolls, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF); |
| registry->RegisterListPref( |
| prefs::kShelfDefaultPinLayoutRollsForTabletFormFactor, |
| PrefRegistry::NO_REGISTRATION_FLAGS); |
| registry->RegisterListPref( |
| prefs::kShelfMallAppPinRolls, |
| user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF); |
| } |
| |
| // TODO(crbug.com/350769496): Fixes bug from M127 beta, can be removed once M127 |
| // is no longer in stable (end of 2024, or mid 2025 is ok). |
| void ChromeShelfPrefs::CleanupPreloadPrefs(PrefService* profile_prefs) { |
| constexpr std::array<const char*, 2> kPrefNames{ |
| prefs::kShelfDefaultPinLayoutRolls, |
| prefs::kShelfDefaultPinLayoutRollsForTabletFormFactor}; |
| |
| for (auto* const pref_name : kPrefNames) { |
| // Deduplicate items in list. |
| ScopedListPrefUpdate list(profile_prefs, pref_name); |
| std::set<base::Value> set; |
| for (const auto& item : *list) { |
| set.insert(item.Clone()); |
| } |
| if (set.size() < list->size()) { |
| list->clear(); |
| for (const auto& item : set) { |
| list->Append(item.Clone()); |
| } |
| } |
| } |
| } |
| |
| void ChromeShelfPrefs::InitLocalPref(PrefService* prefs, |
| const char* local, |
| const char* synced) { |
| // Ash's prefs *should* have been propagated to Chrome by now, but maybe not. |
| // This belongs in Ash, but it can't observe syncing changes: crbug.com/774657 |
| if (prefs->FindPreference(local) && prefs->FindPreference(synced) && |
| !prefs->FindPreference(local)->HasUserSetting()) { |
| prefs->SetString(local, prefs->GetString(synced)); |
| } |
| } |
| |
| // Helper that extracts app list from policy preferences. |
| std::vector<std::string> ChromeShelfPrefs::GetAppsPinnedByPolicy( |
| Profile* profile) { |
| CHECK(profile); |
| const base::Value::List& policy_apps = |
| profile->GetPrefs()->GetList(prefs::kPolicyPinnedLauncherApps); |
| if (policy_apps.empty()) { |
| return {}; |
| } |
| |
| std::vector<std::string> policy_entries; |
| for (const auto& policy_app : policy_apps) { |
| if (!policy_app.is_dict()) { |
| continue; |
| } |
| const std::string* policy_entry = policy_app.GetDict().FindString( |
| ChromeShelfPrefs::kPinnedAppsPrefAppIDKey); |
| if (!policy_entry) { |
| LOG(ERROR) << "Cannot extract policy app info from prefs."; |
| continue; |
| } |
| |
| if (ash::DemoSession::Get() && |
| !ash::DemoSession::Get()->ShouldShowAppInShelf(*policy_entry)) { |
| continue; |
| } |
| |
| policy_entries.push_back(apps_util::TransformRawPolicyId(*policy_entry)); |
| } |
| |
| if (policy_entries.empty()) { |
| return {}; |
| } |
| |
| std::vector<std::string> results; |
| for (const auto& policy_entry : policy_entries) { |
| std::vector<std::string> app_ids = |
| apps_util::GetAppIdsFromPolicyId(profile, policy_entry); |
| if (app_ids.empty()) { |
| LOG(WARNING) << "No matching app(s) found for |policy_entry| = " |
| << policy_entry; |
| continue; |
| } |
| base::Extend(results, std::move(app_ids)); |
| } |
| |
| return results; |
| } |
| |
| std::vector<ash::ShelfID> ChromeShelfPrefs::GetPinnedAppsFromSync( |
| ShelfControllerHelper* helper) { |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| |
| // Some unit tests may not have it or service or helper may not be |
| // initialized. |
| if (!syncable_service || !syncable_service->IsInitialized() || !helper) { |
| return std::vector<ash::ShelfID>(); |
| } |
| |
| if (!sync_service_observer_.IsObserving()) { |
| sync_service_observer_.Observe(syncable_service); |
| } |
| |
| if (ShouldPerformConsistencyMigrations()) { |
| needs_consistency_migrations_ = false; |
| MigrateFilesChromeAppToSWA(); |
| EnsureChromePinned(); |
| EnsureProjectorShelfPinConsistency(); |
| } |
| |
| // This migration must be run outside of the consistency migrations block |
| // since the timing can occur later, after apps have been synced. |
| if (!DidAddDefaultApps() && ShouldAddDefaultApps()) { |
| AddDefaultApps(); |
| } |
| |
| if (IsSafeToApplyDefaultPinLayout(profile_)) { |
| AddGeminiAppPinIfNeeded(profile_, helper, syncable_service); |
| AddNotebookLmAppPinIfNeeded(profile_, syncable_service); |
| AddMallPinIfNeeded(profile_, syncable_service); |
| } |
| |
| // Handle pins, forced by policy. In case Chrome is first app they are added |
| // after Chrome, otherwise they are added to the front. Note, we handle apps |
| // that may not be currently on device. At this case pin position would be |
| // preallocated and apps will appear on shelf in deterministic order, even if |
| // their install order differ. |
| std::vector<std::string> policy_pinned_apps = GetAppsPinnedByPolicy(profile_); |
| InsertPinsAfterChromeAndBeforeFirstPinnedApp(syncable_service, |
| policy_pinned_apps); |
| |
| // Pin preload apps only if none are set by policy. |
| if (!DidAddPreloadApps() && policy_pinned_apps.empty() && |
| IsSafeToApplyDefaultPinLayout(profile_)) { |
| PinPreloadApps(); |
| } |
| |
| std::vector<PinInfo> pin_infos; |
| |
| // Empty pins indicates that sync based pin model is used for the first |
| // time. In the normal workflow we have at least Chrome browser pin info. |
| for (const auto& [item_id, sync_item] : syncable_service->sync_items()) { |
| // A null ordinal means the item has been unpinned. |
| if (!sync_item->item_pin_ordinal.IsValid()) { |
| continue; |
| } |
| |
| const std::string& app_id = item_id; |
| |
| // All sync items must be valid app service apps to be added to the shelf |
| // with the exception of ash-chrome, which for legacy reasons does not use |
| // the app service. |
| bool is_ash_chrome = app_id == app_constants::kChromeAppId; |
| if (!is_ash_chrome && !helper->IsValidIDForCurrentUser(app_id) && |
| !ShelfControllerHelper::IsPromiseApp(profile_, app_id)) { |
| continue; |
| } |
| |
| pin_infos.emplace_back(app_id, sync_item->item_pin_ordinal); |
| } |
| |
| // Sort pins according their ordinals. |
| std::ranges::sort(pin_infos, syncer::StringOrdinal::LessThanFn(), |
| &PinInfo::item_ordinal); |
| |
| // Convert to ShelfID array. |
| std::vector<ash::ShelfID> pins; |
| std::ranges::transform( |
| pin_infos, std::back_inserter(pins), |
| [](const auto& pin_info) { return ash::ShelfID(pin_info.app_id); }); |
| |
| return pins; |
| } |
| |
| void ChromeShelfPrefs::RemovePinPosition(const ash::ShelfID& shelf_id) { |
| DCHECK(profile_); |
| |
| const std::string& app_id = shelf_id.app_id; |
| if (!shelf_id.launch_id.empty()) { |
| VLOG(2) << "Syncing remove pin for '" << app_id |
| << "' with non-empty launch id '" << shelf_id.launch_id |
| << "' is not supported."; |
| return; |
| } |
| DCHECK(!app_id.empty()); |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_) |
| ->RemovePinPosition(app_id); |
| } |
| |
| void ChromeShelfPrefs::SetPinPosition( |
| const ash::ShelfID& shelf_id, |
| const ash::ShelfID& shelf_id_before, |
| base::span<const ash::ShelfID> shelf_ids_after) { |
| const std::string& app_id = shelf_id.app_id; |
| |
| if (!shelf_id.launch_id.empty()) { |
| VLOG(2) << "Syncing set pin for '" << app_id |
| << "' with non-empty launch id '" << shelf_id.launch_id |
| << "' is not supported."; |
| return; |
| } |
| |
| const std::string& app_id_before = shelf_id_before.app_id; |
| |
| DCHECK(!app_id.empty()); |
| DCHECK_NE(app_id, app_id_before); |
| |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| // Some unit tests may not have this service. |
| if (!syncable_service) { |
| return; |
| } |
| |
| syncer::StringOrdinal position_before = |
| app_id_before.empty() ? syncer::StringOrdinal() |
| : syncable_service->GetPinPosition(app_id_before); |
| syncer::StringOrdinal position_after; |
| for (const auto& shelf_id_after : shelf_ids_after) { |
| const std::string& app_id_after = shelf_id_after.app_id; |
| DCHECK_NE(app_id_after, app_id); |
| DCHECK_NE(app_id_after, app_id_before); |
| syncer::StringOrdinal position = |
| syncable_service->GetPinPosition(app_id_after); |
| DCHECK(position.IsValid()); |
| if (!position.IsValid()) { |
| LOG(ERROR) << "Sync pin position was not found for " << app_id_after; |
| continue; |
| } |
| if (!position_before.IsValid() || !position.Equals(position_before)) { |
| position_after = position; |
| break; |
| } |
| } |
| |
| syncer::StringOrdinal pin_position = |
| CreateBetween(position_before, position_after); |
| syncable_service->SetPinPosition(app_id, pin_position); |
| } |
| |
| void ChromeShelfPrefs::SetShouldAddDefaultAppsForTest(bool value) { |
| should_add_default_apps_for_test = value; |
| } |
| |
| void ChromeShelfPrefs::MigrateFilesChromeAppToSWA() { |
| PrefService* prefs = profile_->GetPrefs(); |
| if (prefs->GetBoolean(ash::prefs::kFilesAppUIPrefsMigrated)) { |
| return; |
| } |
| |
| // Avoid migrating the user prefs (even if the migration fails) to avoid |
| // overriding preferences that a user may set on the SWA explicitly. |
| prefs->SetBoolean(ash::prefs::kFilesAppUIPrefsMigrated, true); |
| |
| using MigrationStatus = file_manager::FileManagerPrefsMigrationStatus; |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| if (!syncable_service->GetSyncItem(extension_misc::kFilesManagerAppId)) { |
| base::UmaHistogramEnumeration(file_manager::kPrefsMigrationStatusUMA, |
| MigrationStatus::kFailNoExistingPreferences); |
| return; |
| } |
| if (!syncable_service->TransferItemAttributes( |
| /*from_app_id=*/extension_misc::kFilesManagerAppId, |
| /*to_app_id=*/file_manager::kFileManagerSwaAppId)) { |
| base::UmaHistogramEnumeration(file_manager::kPrefsMigrationStatusUMA, |
| MigrationStatus::kFailMigratingPreferences); |
| return; |
| } |
| |
| base::UmaHistogramEnumeration(file_manager::kPrefsMigrationStatusUMA, |
| MigrationStatus::kSuccess); |
| } |
| |
| void ChromeShelfPrefs::EnsureProjectorShelfPinConsistency() { |
| PrefService* prefs = profile_->GetPrefs(); |
| if (prefs->GetBoolean(ash::prefs::kProjectorSWAUIPrefsMigrated)) { |
| return; |
| } |
| |
| prefs->SetBoolean(ash::prefs::kProjectorSWAUIPrefsMigrated, true); |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_) |
| ->TransferItemAttributes(ash::kChromeUITrustedProjectorSwaAppIdDeprecated, |
| ash::kChromeUIUntrustedProjectorSwaAppId); |
| } |
| |
| void ChromeShelfPrefs::EnsureChromePinned() { |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| EnsurePinnedOrMakeFirst(app_constants::kChromeAppId, syncable_service); |
| } |
| |
| bool ChromeShelfPrefs::DidAddDefaultApps() const { |
| return base::Contains( |
| profile_->GetPrefs()->GetList(GetShelfDefaultPinLayoutPref()), |
| kDefaultPinnedAppsKey); |
| } |
| |
| bool ChromeShelfPrefs::ShouldAddDefaultApps() const { |
| if (should_add_default_apps_for_test) { |
| return true; |
| } |
| // Apply default apps in case profile syncing is done. Otherwise there is a |
| // risk that applied default apps would be overwritten by sync once it is |
| // completed. prefs::kPolicyPinnedLauncherApps overrides any default layout. |
| // This also limits applying experimental configuration only for users who |
| // have the default pin layout specified by |kDefaultPinnedApps| or for |
| // fresh users who have no pin information at all. Default configuration is |
| // not applied if any of experimental layout was rolled. |
| return !profile_->GetPrefs()->HasPrefPath(prefs::kPolicyPinnedLauncherApps) && |
| IsSafeToApplyDefaultPinLayout(profile_); |
| } |
| |
| void ChromeShelfPrefs::AddDefaultApps() { |
| VLOG(1) << "Roll default shelf pin layout " << kDefaultPinnedAppsKey; |
| std::vector<std::string> default_app_ids; |
| for (const char* default_app_id : |
| GetDefaultPinnedAppsForFormFactor(profile_)) { |
| default_app_ids.push_back(default_app_id); |
| } |
| InsertPinsAfterChromeAndBeforeFirstPinnedApp( |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_), |
| default_app_ids); |
| ScopedListPrefUpdate update(profile_->GetPrefs(), |
| GetShelfDefaultPinLayoutPref()); |
| update->Append(kDefaultPinnedAppsKey); |
| } |
| |
| bool ChromeShelfPrefs::DidAddPreloadApps() const { |
| return base::Contains( |
| profile_->GetPrefs()->GetList(GetShelfDefaultPinLayoutPref()), |
| kPreloadPinnedAppsKey); |
| } |
| |
| void ChromeShelfPrefs::PinPreloadApps() { |
| // Only pin once per user. |
| if (pending_preload_apps_.empty() || DidAddPreloadApps()) { |
| return; |
| } |
| |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| |
| for (auto it = pending_preload_apps_.begin(); |
| it != pending_preload_apps_.end();) { |
| // If app is not installed yet, check again later, else delete it from the |
| // pending list. |
| apps::PackageId package_id = *it; |
| std::optional<std::string> app_id = |
| apps_util::GetAppWithPackageId(profile_, package_id); |
| if (!app_id) { |
| ++it; |
| continue; |
| } |
| it = pending_preload_apps_.erase(it); |
| |
| // Ignore if already pinned. |
| if (syncable_service->GetPinPosition(*app_id).IsValid()) { |
| LOG(WARNING) << "Preload already pinned " << package_id; |
| continue; |
| } |
| |
| // Place this app between lhs and rhs, or last if we don't find a match. |
| syncer::StringOrdinal lhs = GetLastPosition(syncable_service); |
| syncer::StringOrdinal rhs; |
| |
| // Find app then search in reverse to find the first app that exists prior |
| // to this app in desired order. If none found, then search forward for the |
| // first app that exists after in desired order. |
| size_t i = 0; |
| for (; i < preload_pin_order_.size(); i++) { |
| if (preload_pin_order_[i] == package_id) { |
| break; |
| } |
| } |
| if (i == preload_pin_order_.size()) { |
| LOG(ERROR) << "Preload pin app not found in pin order " << package_id; |
| continue; |
| } |
| size_t app_index = i; |
| // Find closest prior app and pin after it. |
| for (i = app_index; i > 0; i--) { |
| apps::PackageId app = preload_pin_order_[i - 1]; |
| auto pos = GetAppPosition(profile_, app, syncable_service); |
| if (pos.IsValid()) { |
| lhs = pos; |
| rhs = GetNextPositionItemIdAfter(syncable_service, pos).position; |
| break; |
| } |
| } |
| // If no prior app, then find next subsequent app and pin before it. |
| if (i == 0) { |
| for (i = app_index + 1; i < preload_pin_order_.size(); i++) { |
| apps::PackageId app = preload_pin_order_[i]; |
| auto pos = GetAppPosition(profile_, app, syncable_service); |
| if (pos.IsValid()) { |
| rhs = pos; |
| lhs = GetNextPositionBefore(syncable_service, pos); |
| break; |
| } |
| } |
| } |
| syncer::StringOrdinal position = CreateBetween(lhs, rhs); |
| syncable_service->SetPinPosition(*app_id, position); |
| } |
| |
| // Mark preload pin complete once all apps are installed and pinned. |
| if (pending_preload_apps_.empty()) { |
| SetPreloadPinComplete(profile_); |
| } |
| } |
| |
| void ChromeShelfPrefs::AttachProfile(Profile* profile) { |
| profile_ = profile; |
| needs_consistency_migrations_ = true; |
| sync_service_observer_.Reset(); |
| if (profile_) { |
| CleanupPreloadPrefs(profile_->GetPrefs()); |
| } |
| |
| pending_preload_apps_.clear(); |
| preload_pin_order_.clear(); |
| if (profile_ && !DidAddPreloadApps()) { |
| if (auto* app_preload_service = apps::AppPreloadService::Get(profile_)) { |
| app_preload_service->GetPinApps( |
| base::BindOnce(&ChromeShelfPrefs::OnGetPinPreloadApps, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| } |
| |
| std::string ChromeShelfPrefs::GetPromisePackageIdForSyncItem( |
| const std::string& app_id) { |
| auto* syncable_service = |
| app_list::AppListSyncableServiceFactory::GetForProfile(profile_); |
| |
| // Some unit tests may not have the service or it may not be initialized. |
| if (!syncable_service || !syncable_service->IsInitialized()) { |
| return std::string(); |
| } |
| |
| const app_list::AppListSyncableService::SyncItem* item = |
| syncable_service->GetSyncItem(app_id); |
| return item->promise_package_id; |
| } |
| |
| bool ChromeShelfPrefs::ShouldPerformConsistencyMigrations() const { |
| return needs_consistency_migrations_; |
| } |
| |
| void ChromeShelfPrefs::OnSyncModelUpdated() { |
| needs_consistency_migrations_ = true; |
| } |
| |
| void ChromeShelfPrefs::OnGetPinPreloadApps( |
| const std::vector<apps::PackageId>& pin_apps, |
| const std::vector<apps::PackageId>& pin_order) { |
| pending_preload_apps_ = pin_apps; |
| preload_pin_order_ = pin_order; |
| if (pin_apps.empty() && !DidAddPreloadApps()) { |
| SetPreloadPinComplete(profile_); |
| } |
| } |