chromium/chrome/browser/ash/app_list/app_list_syncable_service.h

// 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.

#ifndef CHROME_BROWSER_ASH_APP_LIST_APP_LIST_SYNCABLE_SERVICE_H_
#define CHROME_BROWSER_ASH_APP_LIST_APP_LIST_SYNCABLE_SERVICE_H_

#include <stddef.h>

#include <map>
#include <memory>
#include <string>
#include <vector>

#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "base/one_shot_event.h"
#include "base/scoped_observation_traits.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_preload_service/preload_app_definition.h"
#include "chrome/browser/ash/app_list/reorder/app_list_reorder_delegate.h"
#include "chrome/browser/sync/glue/sync_start_util.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/sync/model/string_ordinal.h"
#include "components/sync/model/sync_change.h"
#include "components/sync/model/sync_change_processor.h"
#include "components/sync/model/syncable_service.h"
#include "components/sync/protocol/app_list_specifics.pb.h"

class AppListModelUpdater;
class AppServiceAppModelBuilder;
class AppServicePromiseAppModelBuilder;
class ChromeAppListItem;
class Profile;

namespace extensions {
class ExtensionRegistry;
class ExtensionSystem;
}  // namespace extensions

namespace sync_pb {
class AppListSpecifics;
}

namespace user_prefs {
class PrefRegistrySyncable;
}

namespace app_list {
class AppListReorderDelegate;
class AppListSyncModelSanitizer;

// Keyed Service that owns, stores, and syncs an AppListModel for a profile.
class AppListSyncableService : public syncer::SyncableService,
                               public KeyedService,
                               public reorder::AppListReorderDelegate {
 public:
  struct SyncItem {
    SyncItem(const std::string& id,
             sync_pb::AppListSpecifics::AppListItemType type,
             bool is_new);
    SyncItem(const SyncItem&) = delete;
    SyncItem& operator=(const SyncItem&) = delete;
    ~SyncItem();
    const std::string item_id;
    sync_pb::AppListSpecifics::AppListItemType item_type;
    std::string item_name;
    std::string promise_package_id;
    std::string parent_id;
    syncer::StringOrdinal item_ordinal;
    syncer::StringOrdinal item_pin_ordinal;
    ash::IconColor item_color;

    // Indicates whether the item represents a system-created folder - i.e. a
    // folder that was not created explicitly by a user.
    // Unlike other properties, this value is not persisted to local state, nor
    // synced. It reflects the associated ChromeAppListItem state.
    bool is_system_folder = false;

    // Whether the `item_ordinal` should be fixed after initial sync data is
    // received during a user session.
    // This value is preserved in local prefs, but not synced. It helps identify
    // app items added to persistent storage to set default shelf pin ordinal,
    // which may happen before a default app gets installed.
    // If initial data is received before the app is first installed,
    // the item ordinal would be initialized without taking special cases like
    // default item ordinals, or sort order into account - see
    // https://crbug.com/1306913.
    bool empty_item_ordinal_fixable = true;

    // If set with value equal to `item_ordinal`, indicates that the item
    // ordinal should be reset to a value used by default for new apps. Used for
    // default installed apps that need to be added to default app order for new
    // users, but positioned to the front (as if it was a newly installed app)
    // of the app list for existing users. This will be set if the app position
    // is set to the default order before initial sync completes (as initial
    // sync may change whether the  user is considered new or existing).
    std::optional<syncer::StringOrdinal>
        ordinal_to_undo_on_non_empty_initial_sync;

    // Indicates whether the sync item is ephemeral - i.e. an app or a folder
    // that does not persist across sessions. These items have a uniquely
    // generated ID per-session.
    // Sync items that are marked as ephemeral will not persist to local state,
    // nor be synced, in order to avoid growing the App List indefinitely with
    // IDs of obsolete ephemeral items.
    bool is_ephemeral = false;

    // Whether the app was pinned to shelf by the user or not.
    // The eventual consistency (a sufficient amount of time after rollout)
    // we're aspiring to reach here is for this field to be interleaved with the
    // pin ordinal: `item_pin_ordinal.IsValid() <=> is_user_pinned.has_value()`.
    // However, it's okay for this contract to be violated in the meantine.
    //
    //  * missing value indicates that either `item_pin_ordinal` is invalid or
    //    this field is new and hasn't yet been processed by sync.
    //  * `true` means that the app was pinned by the user.
    //    We are using this definition in a relaxed way -- for instance, default
    //    OS apps that are shown in the shelf (like Chrome itself) also have
    //    this set to true.
    //  * `false` means that the app was pinned by PinnedLauncherApps policy.
    //    Note that user pin has priority: if an app was first pinned by the
    //    user and then additionally specified in PinnedLauncherApps, this value
    //    will be set to true.
    std::optional<bool> is_user_pinned;

    // Whether the item is considered new - i.e. first added during the current
    // user session. This will be false if the sync item was created when
    // loading items from local storage, or in response to sync changes.
    const bool is_new;

    std::string ToString() const;
  };

  class Observer : public base::CheckedObserver {
   public:
    // Notifies that sync model was updated.
    virtual void OnSyncModelUpdated() = 0;

    // Notifies the addition or update from the sync items for testing.
    virtual void OnAddOrUpdateFromSyncItemForTest() {}

   protected:
    ~Observer() override;
  };

  // An app list model updater factory function used by tests.
  using ModelUpdaterFactoryCallback =
      base::RepeatingCallback<std::unique_ptr<AppListModelUpdater>(
          reorder::AppListReorderDelegate*)>;

  // Sets an app list model updater factory function for tests. Its lifetime is
  // bound to the lifetime of the returned unique_ptr<>.
  static std::unique_ptr<base::ScopedClosureRunner>
  SetScopedModelUpdaterFactoryForTest(ModelUpdaterFactoryCallback callback);

  using SyncItemMap = std::map<std::string, std::unique_ptr<SyncItem>>;

  // Populates the model when |profile|'s extension system is ready.
  explicit AppListSyncableService(Profile* profile);
  AppListSyncableService(const AppListSyncableService&) = delete;
  AppListSyncableService& operator=(const AppListSyncableService&) = delete;
  ~AppListSyncableService() override;

  // Registers prefs to support local storage.
  static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);

  // Some sync behavior depends on whether or not an app was installed by
  // default (as opposed to e.g. installed via explicit user action). Some
  // tests want the AppListSyncableService to consider an app to be installed
  // by default, without going through the heavyweight process of completely
  // installing an app. These functions facilitate that.
  static bool AppIsDefaultForTest(Profile* profile, const std::string& id);
  static void SetAppIsDefaultForTest(Profile* profile, const std::string& id);

  // Adds |item| to |sync_items_| and |model_|. If a sync item already exists,
  // updates the existing sync item instead.
  void AddItem(std::unique_ptr<ChromeAppListItem> app_item);

  // Removes sync item matching |id|. |is_uninstall| indicates whether the item
  // was removed due to an app uninstall.
  void RemoveItem(const std::string& id, bool is_uninstall);

  // Returns the default position for the OEM folder.
  syncer::StringOrdinal GetDefaultOemFolderPosition() const;

  // Creates a string ordinal that would position an app list item as the last
  // item in the app list.
  syncer::StringOrdinal GetLastPosition() const;

  // Gets a string ordinal that would position an app after the item with the
  // provided `id`.
  syncer::StringOrdinal GetPositionAfterApp(const std::string& id) const;

  // Describes linkage between a promise app item, and an existing app sync
  // item. Promise app will be linked with an existing app when the existing app
  // package ID matches the promise app ID (i.e. when the promise app is
  // installing an app previously installed by the user).
  struct LinkedPromiseAppSyncItem {
    // The ID of the existing sync item linked with the promise app.
    const std::string linked_item_id;
    // The promise app sync item created from the linked sync item attributes.
    const raw_ptr<const SyncItem> promise_item;
  };

  // If an sync item with the provided package ID exists, it creates a sync item
  // for the promise app, and "links" it with the existing sync item.
  // When a promise app item is linked to another sync item, changes to the sync
  // item (e.g. from app list sync) will be applied to the promise app item, and
  // change to promise app item (e.g. from user actions in app list UI) will be
  // applied to the linked sync item.
  // Linkage will be removed when the promise app item gets removed.
  // This can be called multiple times per promise app, and it will return
  // consistent result as long as the linkage is active.
  // If no items that can be linked to the promise app are found, the promise
  // app sync item will not be created, and this will return nullopt.
  std::optional<LinkedPromiseAppSyncItem>
  CreateLinkedPromiseSyncItemIfAvailable(const std::string& promise_package_id);

  // Called when properties of an item may have changed, e.g. default/oem state.
  void UpdateItem(const ChromeAppListItem* app_item);

  // Returns the existing sync item matching |id| or NULL.
  virtual const SyncItem* GetSyncItem(const std::string& id) const;

  // Adds a page break item with the provided ID at the provided position.
  void AddPageBreakItem(const std::string& page_break_id,
                        const syncer::StringOrdinal& position);

  // Transfers app attributes, such as parent folder id, position in App
  // Launcher and pin position on the shelf from one app to another app. Target
  // app defined by |to_app_id| is not required to be present at call time. In
  // which case attributes would be applied once the target app appears on the
  // device. Note, pending attributes are not preserved between the user
  // sessions. This functionality is primarily used for migrating app in case
  // app id is changed but it is required to preserve position in App Launcher
  // and in shelf.
  // Returns true on success and false in case app defined by |from_app_id|
  // does not exist.
  bool TransferItemAttributes(const std::string& from_app_id,
                              const std::string& to_app_id);

  // Sets the name of the folder for OEM apps.
  void SetOemFolderName(const std::string& name);

  // Returns optional pin position for the app specified by |app_id|. If app is
  // not synced or does not have associated pin position then empty ordinal is
  // returned.
  virtual syncer::StringOrdinal GetPinPosition(const std::string& app_id);

  // Sets pin position and how it is pinned for the app specified by |app_id|.
  // |item_pin_ordinal| must be valid.
  // |pinned_by_policy| tells whether this item is pinned to the shelf by the
  // `PinnedLauncherApps` policy.
  virtual void SetPinPosition(const std::string& app_id,
                              const syncer::StringOrdinal& item_pin_ordinal,
                              bool pinned_by_policy);

  // Copies a promise app sync item attributes from a sync item  with
  // `promise_app_id` to a sync item with `target_id`. No-op if the source sync
  // item does not exist. If the target sync item does not exist, it will be
  // created. At the time of writing, used to move a promise app sync item
  // attributes the the sync item associated with the installed app.
  void CopyPromiseItemAttributesToItem(const std::string& promise_app_id,
                                       const std::string& target_id);

  // Sets |is_user_pinned| to false for the given item specified by |item_id|.
  // Item must exist, |item_pin_ordinal| must be valid, and |is_user_pinned|
  // must be unset by the time of the call.
  void SetIsPolicyPinned(const std::string& app_id);

  // Removes pin position for the app specified by |app_id|.
  virtual void RemovePinPosition(const std::string& app_id);

  // Gets the app list model updater.
  AppListModelUpdater* GetModelUpdater();

  // Returns true if this service was initialized.
  // Virtual for testing.
  virtual bool IsInitialized() const;

  // Returns true if sync was started.
  bool IsSyncing() const;

  // Registers a `callback` to be run from a posted task on completion of the
  // first sync in the session. The `callback` is notified of whether the first
  // sync in the session was thought to be the first sync ever across all
  // ChromeOS devices and sessions for the associated user. This method is safe
  // to call even after completion of the first sync in the session, in which
  // case the `callback` will be run from a task posted immediately.
  // NOTE: Virtual for testing.
  virtual void OnFirstSync(
      base::OnceCallback<void(bool was_first_sync_ever)> callback);

  const std::string& GetOemFolderNameForTest() const {
    return oem_folder_name_;
  }

  // Receives launcher ordering when AppPreloadService is ready, and merges with
  // `preload_service_ordinals_` to precalculate the ordinals for any of the
  // default apps to be installed by APS.
  void OnGetLauncherOrdering(const apps::LauncherOrdering& launcher_ordering);

  const std::map<apps::LauncherItem, syncer::StringOrdinal>&
  GetDefaultOrdinalsForTest() const {
    return preload_service_ordinals_;
  }

  void PopulateSyncItemsForTest(std::vector<std::unique_ptr<SyncItem>>&& items);

  virtual const SyncItemMap& sync_items() const;

  // syncer::SyncableService
  void WaitUntilReadyToSync(base::OnceClosure done) override;
  std::optional<syncer::ModelError> MergeDataAndStartSyncing(
      syncer::DataType type,
      const syncer::SyncDataList& initial_sync_data,
      std::unique_ptr<syncer::SyncChangeProcessor> sync_processor) override;
  void StopSyncing(syncer::DataType type) override;
  syncer::SyncDataList GetAllSyncDataForTesting() const;
  std::optional<syncer::ModelError> ProcessSyncChanges(
      const base::Location& from_here,
      const syncer::SyncChangeList& change_list) override;
  base::WeakPtr<SyncableService> AsWeakPtr() override;

  // KeyedService
  void Shutdown() override;

  // reorder::AppListReorderDelegate:
  void SetAppListPreferredOrder(ash::AppListSortOrder order) override;
  syncer::StringOrdinal CalculateGlobalFrontPosition() const override;
  bool CalculateItemPositionInPermanentSortOrder(
      const ash::AppListItemMetadata& metadata,
      syncer::StringOrdinal* target_position) const override;
  ash::AppListSortOrder GetPermanentSortingOrder() const override;

  void set_app_default_positioned_for_new_users_only_for_test(
      const std::string& app_id) {
    app_default_positioned_for_new_users_only_ = app_id;
  }

 private:
  friend class AppListSyncModelSanitizer;
  friend struct base::ScopedObservationTraits<AppListSyncableService,
                                              AppListSyncableService::Observer>;
  class ModelUpdaterObserver;

  // Registers new observers and makes sure that service is started.
  void AddObserverAndStart(Observer* observer);
  void RemoveObserver(Observer* observer);

  // Builds the model once ExtensionService is ready.
  void BuildModel();

  // Returns true if sync has restarted, otherwise runs |flare_|.
  bool SyncStarted();

  // If |app_item| matches an existing sync item, returns it. Otherwise adds
  // |app_item| to |sync_items_| and returns the new item. If |app_item| is
  // invalid returns NULL.
  SyncItem* FindOrAddSyncItem(const ChromeAppListItem* app_item);

  // Creates a sync item for |app_item| and sends an ADD SyncChange event.
  SyncItem* CreateSyncItemFromAppItem(const ChromeAppListItem* app_item);

  // If a sync item for |app_item| already exists, update |app_item| from the
  // sync item, otherwise create a new sync item from |app_item|.
  void AddOrUpdateFromSyncItem(const ChromeAppListItem* app_item);

  // Either uninstalling a default app or remove the REMOVE_DEFAULT sync item.
  // Returns true if the app is removed. Otherwise deletes the existing sync
  // item and returns false.
  bool RemoveDefaultApp(const ChromeAppListItem* item, SyncItem* sync_item);

  // Returns whether the delete-sync-item request was for a default app. If
  // true, the |sync_item| is set to REMOVE_DEFAULT and bounced back to the
  // sync server. The caller should abort deleting the |sync_item|.
  bool InterceptDeleteDefaultApp(SyncItem* sync_item);

  // Deletes a sync item from |sync_items_| and sends a DELETE action.
  void DeleteSyncItem(const std::string& item_id);

  // Updates existing entry in |sync_items_| from |app_item|.
  void UpdateSyncItem(const ChromeAppListItem* app_item);

  // Removes sync item matching |id|.
  void RemoveSyncItem(const std::string& id);

  // Updates folder items that may get created during initial sync.
  void ResolveFolderPositions();

  // Removes any empty SyncItem folders and deletes them from sync. Called
  // after a sync item is removed (which may result in an empty folder).
  void PruneEmptySyncFolders();

  // Creates or updates a SyncItem from |specifics|.
  void ProcessSyncItemSpecifics(const sync_pb::AppListSpecifics& specifics);

  // Handles a newly created sync item (e.g. creates a new AppItem and adds it
  // to the model or uninstalls a deleted default item.
  void ProcessNewSyncItem(SyncItem* sync_item);

  // Handles an existing sync item.
  void ProcessExistingSyncItem(SyncItem* sync_item);

  // Sends ADD or CHANGED for sync item.
  void SendSyncChange(SyncItem* sync_item,
                      syncer::SyncChange::SyncChangeType sync_change_type);

  // Returns an existing sync item corresponding to `item_id` or NULL.
  SyncItem* FindSyncItem(const std::string& item_id);

  // Creates a new sync item for |item_id|.
  SyncItem* CreateSyncItem(const std::string& item_id,
                           sync_pb::AppListSpecifics::AppListItemType item_type,
                           bool is_new);

  // Deletes a SyncItem matching |specifics|.
  void DeleteSyncItemSpecifics(const sync_pb::AppListSpecifics& specifics);

  // Returns true if an extension matching |id| exists and was installed by
  // an OEM (extension->was_installed_by_oem() is true).
  bool AppIsOem(const std::string& id);

  // Initializes sync items from the local storage while sync service is not
  // enabled.
  void InitFromLocalStorage();

  // Helper that notifies observers that sync model has been updated.
  void NotifyObserversSyncUpdated();

  // Handles model update start/finish.
  void HandleUpdateStarted();
  void HandleUpdateFinished(bool clean_up_after_init_sync);

  // Returns true if extension service is ready.
  bool IsExtensionServiceReady() const;

  // Returns a list of top level sync items sorted by item ordinal.
  std::vector<SyncItem*> GetSortedTopLevelSyncItems() const;

  // Remove leading, trailing and duplicate "page break" items in sorted top
  // level item list.
  void PruneRedundantPageBreakItems();

  // Applies sync changes to the local item.
  void UpdateSyncItemFromSync(const sync_pb::AppListSpecifics& specifics,
                              AppListSyncableService::SyncItem* item);

  // Applies changes from the local item to sync item.
  bool UpdateSyncItemFromAppItem(const ChromeAppListItem* app_item,
                                 AppListSyncableService::SyncItem* sync_item);

  // If `new_item` is found in AppPreloadServer `launcher_ordering`, this
  // function returns true and sets `position`. Additionally sets `folder_id`,
  // `folder_name`, and `folder_position` if the item is not in the root folder.
  bool GetAppPreloadServiceInfo(const ChromeAppListItem* new_item,
                                syncer::StringOrdinal* position,
                                std::string* folder_id,
                                std::string* folder_name,
                                syncer::StringOrdinal* folder_position) const;

  // Sets OEM folder name if any OEM folder is specified in the root folder.
  void SetOemFolderNameFromAppPreloadService(
      const apps::LauncherOrdering& launcher_ordering);

  // Initializes `new_item`'s position. This function should be called before
  // adding `new_item` to `model_updater_`.
  void InitNewItemPosition(ChromeAppListItem* new_item);

  // Sets position, folder id and pin position for the app |app_id|. Attributes
  // are taken from the sync item |attributes|. This generates sync update and
  // notifies app models and Chrome shelf controller that are automatically
  // refreshed.
  void ApplyAppAttributes(const std::string& app_id,
                          std::unique_ptr<SyncItem> attributes);

  // Creates a `ChromeAppListItem` and a sync item for the specified folder if
  // it doesn't already exist. `folder_position` is used if it is valid, and
  // this item does not already have sync data.
  void EnsureFolderExists(const std::string& folder_id,
                          const std::string& folder_name,
                          syncer::StringOrdinal folder_position);

  // Creates or updates a GuestOS folder's sync data if the folder is
  // missing.
  void MaybeAddOrUpdateGuestOsFolderSyncData(const std::string& folder_id);

  // Creates a folder if the parent folder is missing before adding `app_item`.
  // Returns true if the folder already existed, or if it got created. Returns
  // false if the method failed to ensure the folder existence.
  bool MaybeCreateFolderBeforeAddingItem(ChromeAppListItem* app_item,
                                         const std::string& folder_id);

  // Returns whether the app with `app_id` should be positioned in the default
  // app order for new users only (for existing users, the app will be added to
  // front of the app list when installed).
  bool IsAppDefaultPositionedForNewUsersOnly(const std::string& app_id) const;

  raw_ptr<Profile> profile_;
  raw_ptr<extensions::ExtensionSystem> extension_system_;
  raw_ptr<extensions::ExtensionRegistry> extension_registry_;
  std::unique_ptr<AppListModelUpdater> model_updater_;
  std::unique_ptr<ModelUpdaterObserver> model_updater_observer_;
  std::unique_ptr<AppListSyncModelSanitizer> sync_model_sanitizer_;

  std::unique_ptr<AppServiceAppModelBuilder> app_service_apps_builder_;
  std::unique_ptr<AppServicePromiseAppModelBuilder>
      app_service_promise_apps_builder_;
  std::unique_ptr<syncer::SyncChangeProcessor> sync_processor_;
  SyncItemMap sync_items_;
  // Map that keeps pending request to transfer attributes from one app to
  // another.
  SyncItemMap pending_transfer_map_;
  syncer::SyncableService::StartSyncFlare flare_;
  bool local_state_initially_empty_ = false;
  bool initial_sync_data_processed_ = false;
  bool first_app_list_sync_ = true;
  // Whether OEM folder position is set to a provisional value - the default OEM
  // folder position depends on whether sync data contains any non-default apps.
  // If an OEM app gets installed before initial app lists sync data is
  // processed, the OEM folder position may be incorrect due to unknown sync
  // data state, and has to be recalculated when initial sync gets processed -
  // this variable is used to detect this state.
  bool oem_folder_using_provisional_default_position_ = false;
  std::string oem_folder_name_;
  base::OnceClosure wait_until_ready_to_sync_cb_;

  // Whether the first sync in the session was thought to be the first sync ever
  // across all ChromeOS devices and sessions for the associated user. Note that
  // this value is absent until completion of the first sync in the session.
  std::optional<bool> first_sync_was_first_sync_ever_;

  // Map from a promise app item to an app sync item linked with the promise app
  // - created by `CreateLinkedPromiseSyncItemIfAvailable()`.
  std::map<std::string, std::string> items_linked_to_promise_item_;

  // Used in tests to add an extra app whose default position is used for new
  // users only. `IsAppDefaultPositionedForNewUsersOnly()` will return true for
  // this app.
  std::optional<std::string> app_default_positioned_for_new_users_only_;

  // Launcher ordering from AppPreloadService.
  apps::LauncherOrdering preload_service_order_;

  // Map of ordinals for AppPreloadService ordering.
  std::map<apps::LauncherItem, syncer::StringOrdinal> preload_service_ordinals_;

  // List of observers.
  base::ObserverList<Observer> observer_list_;
  base::OneShotEvent on_first_sync_;

  base::WeakPtrFactory<AppListSyncableService> weak_ptr_factory_{this};
};

}  // namespace app_list

namespace base {

template <>
struct ScopedObservationTraits<app_list::AppListSyncableService,
                               app_list::AppListSyncableService::Observer> {
  static void AddObserver(
      app_list::AppListSyncableService* source,
      app_list::AppListSyncableService::Observer* observer) {
    source->AddObserverAndStart(observer);
  }
  static void RemoveObserver(
      app_list::AppListSyncableService* source,
      app_list::AppListSyncableService::Observer* observer) {
    source->RemoveObserver(observer);
  }
};

}  // namespace base

#endif  // CHROME_BROWSER_ASH_APP_LIST_APP_LIST_SYNCABLE_SERVICE_H_