chromium/chrome/browser/ui/ash/shelf/chrome_shelf_prefs.cc

// 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_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.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/ranges/algorithm.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/crosapi/browser_util.h"
#include "chrome/browser/ash/file_manager/app_id.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/default_pinned_apps/default_pinned_apps.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/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/pref_names.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();
}

// Template for GetNextPositionAfter() 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>
syncer::StringOrdinal GetAdjacentPosition(
    app_list::AppListSyncableService* syncable_service,
    const syncer::StringOrdinal& position,
    bool exclude_chrome,
    Compare compare) {
  syncer::StringOrdinal 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 ||
                           item_id == app_constants::kLacrosAppId ||
                           item_id == app_constants::kAshDebugBrowserAppId)) {
      continue;
    }
    if (position.IsValid() && !compare(position, sync_item->item_pin_ordinal)) {
      continue;
    }

    if (!result.IsValid() || compare(sync_item->item_pin_ordinal, result)) {
      result = sync_item->item_pin_ordinal;
    }
  }
  return result;
}

// Returns the next pin after `position`.
syncer::StringOrdinal GetNextPositionAfter(
    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 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);
      });
}

// 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 GetNextPositionAfter(syncable_service, invalid, exclude_chrome);
}

// 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(app_id == app_constants::kChromeAppId ||
        app_id == app_constants::kLacrosAppId);
  syncer::StringOrdinal position = syncable_service->GetPinPosition(app_id);
  if (!position.IsValid()) {
    position = CreateFirstPinPosition(syncable_service);
    syncable_service->SetPinPosition(app_id, position,
                                     /*is_policy_initiated=*/false);
  }
}

// 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;
}

bool IsOnlyPolicyPinned(app_list::AppListSyncableService::SyncItem* sync_item) {
  return sync_item->is_user_pinned.has_value() &&
         !sync_item->is_user_pinned.value() &&
         ash::features::IsRemoveStalePolicyPinnedAppsFromShelfEnabled();
}

// In order to ensure that the chrome icon in the shelf is consistent across
// devices, we must apply the following rules:
// (1) If lacros is the only web-browser (lacros_only), transform [sync id]
// kChromeAppId <-> [shelf id] kLacrosAppId
// (2) If lacros is the only web-browser and ash debug browser is enabled,
// transform [sync id] kAshDebugBrowserAppId <-> [shelf id] kChromeAppId
std::string GetShelfId(const std::string& sync_id) {
  if (!crosapi::browser_util::IsAshWebBrowserEnabled()) {
    if (sync_id == app_constants::kChromeAppId) {
      return app_constants::kLacrosAppId;
    }
    if (ash::switches::IsAshDebugBrowserEnabled() &&
        sync_id == app_constants::kAshDebugBrowserAppId) {
      return app_constants::kChromeAppId;
    }
  }

  return sync_id;
}

// In order to ensure that the chrome icon in the shelf is consistent across
// devices, we must apply the following rules:
// (1) If lacros is the only web-browser (lacros_only), transform [shelf id]
// kLacrosAppId <-> [sync id] kChromeAppId
// (2) If lacros is the only web-browser and ash debug browser is enabled,
// transform [shelf id] kChromeAppId <-> [sync id] kAshDebugBrowserAppId
std::string GetSyncId(const std::string& shelf_id) {
  if (!crosapi::browser_util::IsAshWebBrowserEnabled()) {
    if (shelf_id == app_constants::kLacrosAppId) {
      return app_constants::kChromeAppId;
    }
    if (ash::switches::IsAshDebugBrowserEnabled() &&
        shelf_id == app_constants::kChromeAppId) {
      return app_constants::kAshDebugBrowserAppId;
    }
  }

  return shelf_id;
}

// 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,
    bool is_policy_initiated) {
  // 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()) {
      // If `is_user_pinned` is currently unknown but the incoming pin is
      // triggered by a change to policy, set `is_user_pinned` to false.
      if (is_policy_initiated && !sync_item->is_user_pinned.has_value() &&
          ash::features::IsRemoveStalePolicyPinnedAppsFromShelfEnabled()) {
        syncable_service->SetIsPolicyPinned(app_id);
      }
      continue;
    }
    const syncer::StringOrdinal position = after.CreateBetween(before);
    syncable_service->SetPinPosition(app_id, position, is_policy_initiated);

    // Shift after position, next policy pin position will be created after
    // current item.
    after = position;
  }
}

void AddContainerAppPinIfNeeded(
    Profile* profile,
    ShelfControllerHelper* helper,
    app_list::AppListSyncableService* syncable_service) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  if (!chromeos::features::IsContainerAppPreinstallEnabled()) {
    return;
  }

  if (!profile->GetPrefs()
           ->GetList(prefs::kShelfContainerAppPinRolls)
           .empty()) {
    return;
  }

  const std::string app_id = web_app::kContainerAppId;

  if (!helper->IsAppDefaultInstalled(profile, app_id)) {
    return;
  }

  const app_list::AppListSyncableService::SyncItem* sync_item =
      syncable_service->GetSyncItem(app_id);
  if (sync_item && sync_item->item_pin_ordinal.IsValid()) {
    if (sync_item->is_user_pinned.value_or(true)) {
      ScopedListPrefUpdate update(profile->GetPrefs(),
                                  prefs::kShelfContainerAppPinRolls);
      update->Append("v1");
    }
    return;
  }

  // Pin the container app before chrome.
  syncable_service->SetPinPosition(app_id,
                                   CreateFirstPinPosition(syncable_service),
                                   /*is_policy_initiated=*/false);
  {
    ScopedListPrefUpdate update(profile->GetPrefs(),
                                prefs::kShelfContainerAppPinRolls);
    update->Append("v1");
  }
#endif  // GOOGLE_CHROME_BRANDING
}

// Ensures the Mall app is pinned to the shelf after Chrome, when Mall is
// enabled.
void AddMallPinIfNeeded(Profile* profile,
                        app_list::AppListSyncableService* syncable_service) {
  if (!base::FeatureList::IsEnabled(chromeos::features::kCrosMall)) {
    return;
  }

  // When Mall SWA is disabled, always pin the Mall web app if it is not
  // already pinned. There is no option to unpin the web app.
  if (!chromeos::features::IsCrosMallSwaEnabled()) {
    InsertPinsAfterChromeAndBeforeFirstPinnedApp(syncable_service,
                                                 {{web_app::kMallAppId}},
                                                 /*is_policy_initiated=*/false);
    return;
  }

  // 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;
  }

  InsertPinsAfterChromeAndBeforeFirstPinnedApp(syncable_service,
                                               {{ash::kMallSystemAppId}},
                                               /*is_policy_initiated=*/false);

  ScopedListPrefUpdate update(profile->GetPrefs(),
                              prefs::kShelfMallAppPinRolls);
  update->Append("v1");
}

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::kShelfContainerAppPinRolls,
      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_)) {
    AddContainerAppPinIfNeeded(profile_, helper, 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,
                                               /*is_policy_initiated=*/true);

  // Pin preload apps only if none are set by policy.
  if (!DidAddPreloadApps() && policy_pinned_apps.empty() &&
      IsSafeToApplyDefaultPinLayout(profile_)) {
    PinPreloadApps();
  }

  // If Lacros is enabled and allowed for this user type, ensure the Lacros icon
  // is pinned. Lacros doesn't support multi-signin, so only add the icon for
  // the primary user.
  if (crosapi::browser_util::IsLacrosEnabled() &&
      ash::ProfileHelper::IsPrimaryProfile(profile_)) {
    syncer::StringOrdinal lacros_position =
        syncable_service->GetPinPosition(app_constants::kLacrosAppId);
    if (!lacros_position.IsValid()) {
      // If Lacros isn't already pinned, add it to the right of the Chrome icon.
      InsertPinsAfterChromeAndBeforeFirstPinnedApp(
          syncable_service, {{app_constants::kLacrosAppId}},
          /*is_policy_initiated=*/false);
    }
  }

  std::vector<std::string> policy_delta_remove_from_shelf;

  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;
    }

    // kChromeAppId is the only valid sync ID for the browser.
    if (item_id == app_constants::kLacrosAppId) {
      continue;
    }

    std::string app_id = GetShelfId(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;
    }

    // Prune apps that used to be policy-pinned (`is_user_pinned = false`), but
    // are not a part of the policy anymore.
    if (!is_ash_chrome && IsOnlyPolicyPinned(sync_item.get()) &&
        !base::Contains(policy_pinned_apps, item_id) &&
        !ShelfControllerHelper::IsPromiseApp(profile_, app_id)) {
      policy_delta_remove_from_shelf.push_back(item_id);
      continue;
    }
    pin_infos.emplace_back(std::move(app_id), sync_item->item_pin_ordinal);
  }

  for (const auto& item_id : policy_delta_remove_from_shelf) {
    syncable_service->RemovePinPosition(item_id);
  }

  // Sort pins according their ordinals.
  base::ranges::sort(pin_infos, syncer::StringOrdinal::LessThanFn(),
                     &PinInfo::item_ordinal);

  // Convert to ShelfID array.
  std::vector<ash::ShelfID> pins;
  base::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,
    bool pinned_by_policy) {
  const std::string app_id = GetSyncId(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 = GetSyncId(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) {
    std::string app_id_after = GetSyncId(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, pinned_by_policy);
}

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_);
  // If ash is the only web browser or if lacros is the only web browser, ensure
  // that ash-chrome is pinned. The sync<->shelf translation layer ensures that
  // we will use the appropriate shelf id.
  if (!crosapi::browser_util::IsLacrosEnabled() ||
      !crosapi::browser_util::IsAshWebBrowserEnabled()) {
    EnsurePinnedOrMakeFirst(app_constants::kChromeAppId, syncable_service);
    return;
  }

  // Otherwise, we are in a transition situation where both the ash and lacros
  // web browsers are available. To ensure consistency with legacy behavior, we
  // ensure both web browsers are pinned.
  EnsurePinnedOrMakeFirst(app_constants::kLacrosAppId, syncable_service);
  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, /*is_policy_initiated=*/false);
  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 = GetNextPositionAfter(syncable_service, pos);
        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,
                                     /*is_policy_initiated=*/false);
  }

  // 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) {
  if (!ash::features::ArePromiseIconsEnabled()) {
    return std::string();
  }

  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_);
  }
}