chromium/components/desks_storage/core/desk_template_conversion.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/desks_storage/core/desk_template_conversion.h"

#include <optional>
#include <string_view>

#include "base/containers/fixed_flat_set.h"
#include "base/json/json_reader.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "chromeos/ui/base/window_state_type.h"
#include "components/app_constants/constants.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/restore_data.h"
#include "components/app_restore/window_info.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/sync/protocol/proto_enum_conversions.h"
#include "components/sync_device_info/device_info_proto_enum_util.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_info.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "ui/gfx/geometry/rect.h"

#if !BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/cpp/lacros_startup_state.h"  // nogncheck
#endif  // !BUILDFLAG(IS_CHROMEOS_LACROS)

namespace {

using SyncWindowOpenDisposition =
    sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition;
using SyncLaunchContainer = sync_pb::WorkspaceDeskSpecifics_LaunchContainer;
using GroupColor = tab_groups::TabGroupColorId;
using BrowserAppTab =
    sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_BrowserAppTab;
using BrowserAppWindow = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow;
using ArcApp = sync_pb::WorkspaceDeskSpecifics_ArcApp;
using ArcAppWindowSize = sync_pb::WorkspaceDeskSpecifics_ArcApp_WindowSize;
using ash::DeskTemplate;
using ash::DeskTemplateSource;
using ash::DeskTemplateType;
using SyncDeskType = sync_pb::WorkspaceDeskSpecifics_DeskType;
using WindowState = sync_pb::WorkspaceDeskSpecifics_WindowState;
using WindowBound = sync_pb::WorkspaceDeskSpecifics_WindowBound;
using LaunchContainer = sync_pb::WorkspaceDeskSpecifics_LaunchContainer;
// Use name prefixed with Sync here to avoid name collision with original class
// which isn't defined in a namespace.
using SyncWindowOpenDisposition =
    sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition;
using ProgressiveWebApp = sync_pb::WorkspaceDeskSpecifics_ProgressiveWebApp;
using ChromeApp = sync_pb::WorkspaceDeskSpecifics_ChromeApp;
using WorkspaceDeskSpecifics_App = sync_pb::WorkspaceDeskSpecifics_App;
using SyncTabGroup = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_TabGroup;
using SyncTabGroupColor = sync_pb::WorkspaceDeskSpecifics_TabGroupColor;
using TabGroupColor = tab_groups::TabGroupColorId;

// JSON value keys.
constexpr char kActiveTabIndex[] = "active_tab_index";
constexpr char kAppId[] = "app_id";
constexpr char kApps[] = "apps";
constexpr char kAppName[] = "app_name";
constexpr char kAppType[] = "app_type";
constexpr char kAppTypeArc[] = "ARC";
constexpr char kAppTypeArcAdminFormat[] = "arc";
constexpr char kAppTypeBrowser[] = "BROWSER";
constexpr char kAppTypeBrowserAdminFormat[] = "browser";
constexpr char kAppTypeChrome[] = "CHROME_APP";
constexpr char kAppTypeChromeAdminFormat[] = "chrome_app";
constexpr char kAppTypeProgressiveWebAdminFormat[] = "progressive_web_app";
constexpr char kAppTypeIsolatedWebAppAdminFormat[] = "isolated_web_app";
constexpr char kAppTypeUnknown[] = "UKNOWN";
constexpr char kAppTypeUnsupported[] = "UNSUPPORTED";
constexpr char kAutoLaunchOnStartup[] = "auto_launch_on_startup";
constexpr char kBoundsInRoot[] = "bounds_in_root";
constexpr char kCreatedTime[] = "created_time_usec";
constexpr char kDesk[] = "desk";
constexpr char kDeskType[] = "desk_type";
constexpr char kDeskTypeTemplate[] = "TEMPLATE";
constexpr char kDeskTypeSaveAndRecall[] = "SAVE_AND_RECALL";
constexpr char kDeskTypeFloatingWorkspace[] = "FLOATING_WORKSPACE";
constexpr char kDeskTypeUnknown[] = "UNKNOWN";
constexpr char kDisplayId[] = "display_id";
constexpr char kEventFlag[] = "event_flag";
constexpr char kFirstNonPinnedTabIndex[] = "first_non_pinned_tab_index";
constexpr char kIsAppTypeBrowser[] = "is_app";
constexpr char kLacrosProfileId[] = "lacros_profile_id";
constexpr char kLaunchContainer[] = "launch_container";
constexpr char kLaunchContainerWindow[] = "LAUNCH_CONTAINER_WINDOW";
constexpr char kLaunchContainerUnspecified[] = "LAUNCH_CONTAINER_UNSPECIFIED";
constexpr char kLaunchContainerPanelDeprecated[] = "LAUNCH_CONTAINER_PANEL";
constexpr char kLaunchContainerTab[] = "LAUNCH_CONTAINER_TAB";
constexpr char kLaunchContainerNone[] = "LAUNCH_CONTAINER_NONE";
constexpr char kMaximumSize[] = "maximum_size";
constexpr char kMinimumSize[] = "minimum_size";
constexpr char kName[] = "name";
constexpr char kOverrideUrl[] = "override_url";
constexpr char kPolicy[] = "policy";
constexpr char kPreMinimizedWindowState[] = "pre_minimized_window_state";
constexpr char kTabRangeFirstIndex[] = "first_index";
constexpr char kTabRangeLastIndex[] = "last_index";
constexpr char kSizeHeight[] = "height";
constexpr char kSizeWidth[] = "width";
constexpr char kSnapPercentage[] = "snap_percent";
constexpr char kTabs[] = "tabs";
constexpr char kTabsAdminFormat[] = "browser_tabs";
constexpr char kTabGroups[] = "tab_groups";
constexpr char kTabUrl[] = "url";
constexpr char kTitle[] = "title";
constexpr char kUpdatedTime[] = "updated_time_usec";
constexpr char kUuid[] = "uuid";
constexpr char kVersion[] = "version";
constexpr char kTabGroupTitleKey[] = "title";
constexpr char kTabGroupColorKey[] = "color";
constexpr char kTabGroupIsCollapsed[] = "is_collapsed";
constexpr char kWindowId[] = "window_id";
constexpr char kWindowBound[] = "window_bound";
constexpr char kWindowBoundHeight[] = "height";
constexpr char kWindowBoundLeft[] = "left";
constexpr char kWindowBoundTop[] = "top";
constexpr char kWindowBoundWidth[] = "width";
constexpr char kWindowOpenDisposition[] = "window_open_disposition";
constexpr char kWindowOpenDispositionUnknown[] = "UNKOWN";
constexpr char kWindowOpenDispositionCurrentTab[] = "CURRENT_TAB";
constexpr char kWindowOpenDispositionSingletonTab[] = "SINGLETON_TAB";
constexpr char kWindowOpenDispositionNewForegroundTab[] = "NEW_FOREGROUND_TAB";
constexpr char kWindowOpenDispositionNewBackgroundTab[] = "NEW_BACKGROUND_TAB";
constexpr char kWindowOpenDispositionNewPopup[] = "NEW_POPUP";
constexpr char kWindowOpenDispositionNewWindow[] = "NEW_WINDOW";
constexpr char kWindowOpenDispositionSaveToDisk[] = "SAVE_TO_DISK";
constexpr char kWindowOpenDispositionOffTheRecord[] = "OFF_THE_RECORD";
constexpr char kWindowOpenDispositionIgnoreAction[] = "IGNORE_ACTION";
constexpr char kWindowOpenDispositionSwitchToTab[] = "SWITCH_TO_TAB";
constexpr char kWindowOpenDispositionNewPictureInPicture[] =
    "NEW_PICTURE_IN_PICTURE";
constexpr char kWindowState[] = "window_state";
constexpr char kWindowStateNormal[] = "NORMAL";
constexpr char kWindowStateMinimized[] = "MINIMIZED";
constexpr char kWindowStateMaximized[] = "MAXIMIZED";
constexpr char kWindowStateFullscreen[] = "FULLSCREEN";
constexpr char kWindowStatePrimarySnapped[] = "PRIMARY_SNAPPED";
constexpr char kWindowStateSecondarySnapped[] = "SECONDARY_SNAPPED";
constexpr char kWindowStateFloated[] = "FLOATED";
constexpr char kZIndex[] = "z_index";

// Valid value sets.
constexpr auto kValidDeskTypes = base::MakeFixedFlatSet<std::string_view>(
    {kDeskTypeTemplate, kDeskTypeSaveAndRecall, kDeskTypeFloatingWorkspace});
constexpr auto kValidLaunchContainers =
    base::MakeFixedFlatSet<std::string_view>(
        {kLaunchContainerWindow, kLaunchContainerPanelDeprecated,
         kLaunchContainerTab, kLaunchContainerNone,
         kLaunchContainerUnspecified});
constexpr auto kValidWindowOpenDispositions =
    base::MakeFixedFlatSet<std::string_view>(
        {kWindowOpenDispositionUnknown, kWindowOpenDispositionCurrentTab,
         kWindowOpenDispositionSingletonTab,
         kWindowOpenDispositionNewForegroundTab,
         kWindowOpenDispositionNewBackgroundTab, kWindowOpenDispositionNewPopup,
         kWindowOpenDispositionNewWindow, kWindowOpenDispositionSaveToDisk,
         kWindowOpenDispositionOffTheRecord, kWindowOpenDispositionIgnoreAction,
         kWindowOpenDispositionSwitchToTab,
         kWindowOpenDispositionNewPictureInPicture});
constexpr auto kValidWindowStates = base::MakeFixedFlatSet<std::string_view>(
    {kWindowStateNormal, kWindowStateMinimized, kWindowStateMaximized,
     kWindowStateFullscreen, kWindowStatePrimarySnapped,
     kWindowStateSecondarySnapped, kWindowStateFloated, kZIndex});
constexpr auto kValidTabGroupColors = base::MakeFixedFlatSet<std::string_view>(
    {tab_groups::kTabGroupColorUnknown, tab_groups::kTabGroupColorGrey,
     tab_groups::kTabGroupColorBlue, tab_groups::kTabGroupColorRed,
     tab_groups::kTabGroupColorYellow, tab_groups::kTabGroupColorGreen,
     tab_groups::kTabGroupColorPink, tab_groups::kTabGroupColorPurple,
     tab_groups::kTabGroupColorCyan, tab_groups::kTabGroupColorOrange});

// Version number.
constexpr int kVersionNum = 1;

// Conversion to desk methods.
bool GetString(const base::Value::Dict& dict,
               const char* key,
               std::string* out) {
  const std::string* value = dict.FindString(key);
  if (!value)
    return false;

  *out = *value;
  return true;
}

bool GetInt(const base::Value::Dict& dict, const char* key, int* out) {
  std::optional<int> value = dict.FindInt(key);
  if (!value)
    return false;

  *out = *value;
  return true;
}

bool GetBool(const base::Value::Dict& dict, const char* key, bool* out) {
  std::optional<bool> value = dict.FindBool(key);
  if (!value)
    return false;

  *out = *value;
  return true;
}

// Get App ID from App proto.
std::string GetJsonAppId(const base::Value::Dict& app) {
  std::string app_type;
  if (GetString(app, kAppType, &app_type) && app_type == kAppTypeBrowser) {
    // Return the primary browser's known app ID.
    const bool is_lacros =
#if BUILDFLAG(IS_CHROMEOS_LACROS)
        true;
#else
        // Note that this will launch the browser as lacros if it is enabled,
        // even if it was saved as a non-lacros window (and vice-versa).
        crosapi::lacros_startup_state::IsLacrosEnabled();
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

    // Browser app has a known app ID.
    return std::string(is_lacros ? app_constants::kLacrosAppId
                                 : app_constants::kChromeAppId);
  }

  // Fall back on a stored app_id (which may or may not be present).
  std::string app_id;
  GetString(app, kAppId, &app_id);

  return app_id;
}

// Convert a TabGroupInfo object to a base::Value::Dict.
base::Value::Dict ConvertTabGroupInfoToDict(
    const tab_groups::TabGroupInfo& group_info) {
  base::Value::Dict tab_group_dict;

  tab_group_dict.Set(kTabRangeFirstIndex,
                     static_cast<int>(group_info.tab_range.start()));
  tab_group_dict.Set(kTabRangeLastIndex,
                     static_cast<int>(group_info.tab_range.end()));
  tab_group_dict.Set(kTabGroupTitleKey,
                     base::UTF16ToUTF8(group_info.visual_data.title()));
  tab_group_dict.Set(kTabGroupColorKey, tab_groups::TabGroupColorToString(
                                            group_info.visual_data.color()));
  tab_group_dict.Set(kTabGroupIsCollapsed,
                     group_info.visual_data.is_collapsed());

  return tab_group_dict;
}

bool IsValidGroupColor(const std::string& group_color) {
  return base::Contains(kValidTabGroupColors, group_color);
}

GroupColor ConvertGroupColorStringToGroupColor(const std::string& group_color) {
  if (group_color == tab_groups::kTabGroupColorGrey) {
    return GroupColor::kGrey;
  } else if (group_color == tab_groups::kTabGroupColorBlue) {
    return GroupColor::kBlue;
  } else if (group_color == tab_groups::kTabGroupColorRed) {
    return GroupColor::kRed;
  } else if (group_color == tab_groups::kTabGroupColorYellow) {
    return GroupColor::kYellow;
  } else if (group_color == tab_groups::kTabGroupColorGreen) {
    return GroupColor::kGreen;
  } else if (group_color == tab_groups::kTabGroupColorPink) {
    return GroupColor::kPink;
  } else if (group_color == tab_groups::kTabGroupColorPurple) {
    return GroupColor::kPurple;
  } else if (group_color == tab_groups::kTabGroupColorCyan) {
    return GroupColor::kCyan;
  } else if (group_color == tab_groups::kTabGroupColorOrange) {
    return GroupColor::kOrange;
    // There is no UNKNOWN equivalent in GroupColor, simply default
    // to grey.
  } else if (group_color == tab_groups::kTabGroupColorUnknown) {
    return GroupColor::kGrey;
  } else {
    NOTREACHED_IN_MIGRATION();
    return GroupColor::kGrey;
  }
}

// Constructs a GroupVisualData from value `group_visual_data` IFF all fields
// are present and valid in the value parameter.  Returns true on success, false
// on failure.
bool MakeTabGroupVisualDataFromDict(
    const base::Value::Dict& tab_group,
    tab_groups::TabGroupVisualData* out_visual_data) {
  std::string tab_group_title;
  std::string group_color_string;
  bool is_collapsed;
  if (GetString(tab_group, kTabGroupTitleKey, &tab_group_title) &&
      GetBool(tab_group, kTabGroupIsCollapsed, &is_collapsed) &&
      GetString(tab_group, kTabGroupColorKey, &group_color_string) &&
      IsValidGroupColor(group_color_string)) {
    *out_visual_data = tab_groups::TabGroupVisualData(
        base::UTF8ToUTF16(tab_group_title),
        ConvertGroupColorStringToGroupColor(group_color_string), is_collapsed);
    return true;
  }

  return false;
}

// Constructs a gfx::Range from value `group_range` IFF all fields are
// present and valid in the value parameter.  Returns true on success, false on
// failure.
bool MakeTabGroupRangeFromDict(const base::Value::Dict& tab_group,
                               gfx::Range* out_range) {
  int32_t range_start;
  int32_t range_end;
  if (GetInt(tab_group, kTabRangeFirstIndex, &range_start) &&
      GetInt(tab_group, kTabRangeLastIndex, &range_end)) {
    *out_range = gfx::Range(range_start, range_end);
    return true;
  }

  return false;
}

// Constructs a TabGroupInfo from `tab_group` IFF all fields are present
// and valid in the value parameter. Returns true on success, false on failure.
std::optional<tab_groups::TabGroupInfo> MakeTabGroupInfoFromDict(
    const base::Value::Dict& tab_group) {
  std::optional<tab_groups::TabGroupInfo> tab_group_info = std::nullopt;

  tab_groups::TabGroupVisualData visual_data;
  gfx::Range range;
  if (MakeTabGroupRangeFromDict(tab_group, &range) &&
      MakeTabGroupVisualDataFromDict(tab_group, &visual_data)) {
    tab_group_info.emplace(range, visual_data);
  }

  return tab_group_info;
}

// Returns true if launch container string value is valid.
bool IsValidLaunchContainer(const std::string& launch_container) {
  return base::Contains(kValidLaunchContainers, launch_container);
}

// Returns a casted apps::LaunchContainer to be set as an app restore data's
// container field.
int32_t StringToLaunchContainer(const std::string& launch_container) {
  if (launch_container == kLaunchContainerWindow) {
    return static_cast<int32_t>(apps::LaunchContainer::kLaunchContainerWindow);
  } else if (launch_container == kLaunchContainerPanelDeprecated) {
    return static_cast<int32_t>(
        apps::LaunchContainer::kLaunchContainerPanelDeprecated);
  } else if (launch_container == kLaunchContainerTab) {
    return static_cast<int32_t>(apps::LaunchContainer::kLaunchContainerTab);
  } else if (launch_container == kLaunchContainerNone) {
    return static_cast<int32_t>(apps::LaunchContainer::kLaunchContainerNone);
  } else if (launch_container == kLaunchContainerUnspecified) {
    return static_cast<int32_t>(apps::LaunchContainer::kLaunchContainerWindow);
    // Dcheck if our container isn't valid.  We should not reach here.
  } else {
    DCHECK(IsValidLaunchContainer(launch_container));
    return static_cast<int32_t>(apps::LaunchContainer::kLaunchContainerWindow);
  }
}

// Returns true if the disposition is a valid value.
bool IsValidWindowOpenDisposition(const std::string& disposition) {
  return base::Contains(kValidWindowOpenDispositions, disposition);
}

// Returns a casted WindowOpenDisposition to be set in the app restore data.
int32_t StringToWindowOpenDisposition(const std::string& disposition) {
  if (disposition == kWindowOpenDispositionUnknown) {
    return static_cast<int32_t>(WindowOpenDisposition::UNKNOWN);
  } else if (disposition == kWindowOpenDispositionCurrentTab) {
    return static_cast<int32_t>(WindowOpenDisposition::CURRENT_TAB);
  } else if (disposition == kWindowOpenDispositionSingletonTab) {
    return static_cast<int32_t>(WindowOpenDisposition::SINGLETON_TAB);
  } else if (disposition == kWindowOpenDispositionNewForegroundTab) {
    return static_cast<int32_t>(WindowOpenDisposition::NEW_FOREGROUND_TAB);
  } else if (disposition == kWindowOpenDispositionNewBackgroundTab) {
    return static_cast<int32_t>(WindowOpenDisposition::NEW_BACKGROUND_TAB);
  } else if (disposition == kWindowOpenDispositionNewPopup) {
    return static_cast<int32_t>(WindowOpenDisposition::NEW_POPUP);
  } else if (disposition == kWindowOpenDispositionNewWindow) {
    return static_cast<int32_t>(WindowOpenDisposition::NEW_WINDOW);
  } else if (disposition == kWindowOpenDispositionSaveToDisk) {
    return static_cast<int32_t>(WindowOpenDisposition::SAVE_TO_DISK);
  } else if (disposition == kWindowOpenDispositionOffTheRecord) {
    return static_cast<int32_t>(WindowOpenDisposition::OFF_THE_RECORD);
  } else if (disposition == kWindowOpenDispositionIgnoreAction) {
    return static_cast<int32_t>(WindowOpenDisposition::IGNORE_ACTION);
  } else if (disposition == kWindowOpenDispositionNewPictureInPicture) {
    return static_cast<int32_t>(WindowOpenDisposition::NEW_PICTURE_IN_PICTURE);

    // Dcheck that the disposition is valid, we should never get here unless
    // the disposition is invalid.
  } else {
    DCHECK(IsValidWindowOpenDisposition(disposition));
    return static_cast<int32_t>(WindowOpenDisposition::UNKNOWN);
  }
}

// Convert App JSON to `app_restore::AppLaunchInfo`.
std::unique_ptr<app_restore::AppLaunchInfo> ConvertJsonToAppLaunchInfo(
    const base::Value::Dict& app) {
  int32_t window_id;
  if (!GetInt(app, kWindowId, &window_id))
    return nullptr;

  const std::string app_id = GetJsonAppId(app);

  if (app_id.empty())
    return nullptr;

  std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
      std::make_unique<app_restore::AppLaunchInfo>(app_id, window_id);

  std::string display_id_string;
  int64_t display_id;
  if (GetString(app, kDisplayId, &display_id_string) &&
      base::StringToInt64(display_id_string, &display_id)) {
    app_launch_info->display_id = display_id;
  }

  std::string launch_container;
  if (GetString(app, kLaunchContainer, &launch_container) &&
      IsValidLaunchContainer(launch_container)) {
    app_launch_info->container = StringToLaunchContainer(launch_container);
  }

  std::string disposition;
  if (GetString(app, kWindowOpenDisposition, &disposition) &&
      IsValidWindowOpenDisposition(disposition)) {
    app_launch_info->disposition = StringToWindowOpenDisposition(disposition);
  }

  std::string app_name;
  if (GetString(app, kAppName, &app_name)) {
    app_launch_info->browser_extra_info.app_name = app_name;
  }

  std::string override_url;
  if (GetString(app, kOverrideUrl, &override_url)) {
    app_launch_info->override_url = GURL(override_url);
  }

  std::string lacros_profile_id_str;
  if (GetString(app, kLacrosProfileId, &lacros_profile_id_str)) {
    uint64_t lacros_profile_id = 0;
    if (base::StringToUint64(lacros_profile_id_str, &lacros_profile_id)) {
      app_launch_info->browser_extra_info.lacros_profile_id = lacros_profile_id;
    }
  }

  // TODO(crbug.com/1311801): Add support for actual event_flag values.
  app_launch_info->event_flag = 0;

  bool app_type_browser;
  if (GetBool(app, kIsAppTypeBrowser, &app_type_browser)) {
    app_launch_info->browser_extra_info.app_type_browser = app_type_browser;
  }

  if (app_id == app_constants::kLacrosAppId ||
      app_id == app_constants::kChromeAppId) {
    int active_tab_index;
    if (GetInt(app, kActiveTabIndex, &active_tab_index)) {
      app_launch_info->browser_extra_info.active_tab_index = active_tab_index;
    }

    int first_non_pinned_tab_index;
    if (GetInt(app, kFirstNonPinnedTabIndex, &first_non_pinned_tab_index)) {
      app_launch_info->browser_extra_info.first_non_pinned_tab_index =
          first_non_pinned_tab_index;
    }

    // Fill in the URL list
    if (const base::Value::List* tabs = app.FindList(kTabs)) {
      for (auto& tab : *tabs) {
        std::string url;
        if (GetString(tab.GetDict(), kTabUrl, &url)) {
          app_launch_info->browser_extra_info.urls.emplace_back(url);
        }
      }
    }

    // Fill the tab groups
    if (const base::Value::List* tab_groups = app.FindList(kTabGroups)) {
      for (auto& tab : *tab_groups) {
        std::optional<tab_groups::TabGroupInfo> tab_group =
            MakeTabGroupInfoFromDict(tab.GetDict());
        if (tab_group.has_value()) {
          app_launch_info->browser_extra_info.tab_group_infos.push_back(
              std::move(tab_group.value()));
        }
      }
    }
  }
  // For Chrome apps and PWAs, the `app_id` is sufficient for identification.

  return app_launch_info;
}

bool IsValidWindowState(const std::string& window_state) {
  return base::Contains(kValidWindowStates, window_state);
}

// Convert JSON string WindowState `state` to ui::WindowShowState used by
// the app_restore::WindowInfo struct.
ui::WindowShowState ToUiWindowState(const std::string& window_state) {
  if (window_state == kWindowStateNormal)
    return ui::WindowShowState::SHOW_STATE_NORMAL;
  else if (window_state == kWindowStateMinimized)
    return ui::WindowShowState::SHOW_STATE_MINIMIZED;
  else if (window_state == kWindowStateMaximized)
    return ui::WindowShowState::SHOW_STATE_MAXIMIZED;
  else if (window_state == kWindowStateFullscreen)
    return ui::WindowShowState::SHOW_STATE_FULLSCREEN;
  else if (window_state == kWindowStatePrimarySnapped)
    return ui::WindowShowState::SHOW_STATE_NORMAL;
  else if (window_state == kWindowStateSecondarySnapped)
    return ui::WindowShowState::SHOW_STATE_NORMAL;
  // We should never reach here unless we have been passed an invalid window
  // state
  DCHECK(IsValidWindowState(window_state));
  return ui::WindowShowState::SHOW_STATE_NORMAL;
}

// Convert JSON string WindowState `state` to chromeos::WindowStateType used by
// the app_restore::WindowInfo struct.
chromeos::WindowStateType ToChromeOsWindowState(
    const std::string& window_state) {
  if (window_state == kWindowStateNormal)
    return chromeos::WindowStateType::kNormal;
  else if (window_state == kWindowStateMinimized)
    return chromeos::WindowStateType::kMinimized;
  else if (window_state == kWindowStateMaximized)
    return chromeos::WindowStateType::kMaximized;
  else if (window_state == kWindowStateFullscreen)
    return chromeos::WindowStateType::kFullscreen;
  else if (window_state == kWindowStatePrimarySnapped)
    return chromeos::WindowStateType::kPrimarySnapped;
  else if (window_state == kWindowStateSecondarySnapped)
    return chromeos::WindowStateType::kSecondarySnapped;
  else if (window_state == kWindowStateFloated)
    return chromeos::WindowStateType::kFloated;

  // We should never reach here unless we have been passed an invalid window
  // state.
  DCHECK(IsValidWindowState(window_state));
  return chromeos::WindowStateType::kNormal;
}

void FillArcExtraWindowInfoFromJson(
    const base::Value::Dict& app,
    app_restore::WindowInfo::ArcExtraInfo* out_window_info) {
  const base::Value::Dict* bounds_in_root = app.FindDict(kBoundsInRoot);
  int top;
  int left;
  int bounds_width;
  int bounds_height;
  if (bounds_in_root && GetInt(*bounds_in_root, kWindowBoundTop, &top) &&
      GetInt(*bounds_in_root, kWindowBoundLeft, &left) &&
      GetInt(*bounds_in_root, kWindowBoundWidth, &bounds_width) &&
      GetInt(*bounds_in_root, kWindowBoundHeight, &bounds_height)) {
    out_window_info->bounds_in_root.emplace(left, top, bounds_width,
                                            bounds_height);
  }

  const base::Value::Dict* maximum_size = app.FindDict(kMaximumSize);
  int max_width;
  int max_height;
  if (maximum_size && GetInt(*maximum_size, kSizeWidth, &max_width) &&
      GetInt(*maximum_size, kSizeHeight, &max_height)) {
    out_window_info->maximum_size.emplace(max_width, max_height);
  }

  const base::Value::Dict* minimum_size = app.FindDict(kMinimumSize);
  int min_width;
  int min_height;
  if (minimum_size && GetInt(*minimum_size, kSizeWidth, &min_width) &&
      GetInt(*minimum_size, kSizeHeight, &min_height)) {
    out_window_info->minimum_size.emplace(min_width, min_height);
  }
}

// Fill `out_window_info` with information from JSON `app`.
void FillWindowInfoFromJson(const base::Value::Dict& app,
                            app_restore::WindowInfo* out_window_info) {
  std::string window_state;
  chromeos::WindowStateType cros_window_state =
      chromeos::WindowStateType::kDefault;
  if (GetString(app, kWindowState, &window_state) &&
      IsValidWindowState(window_state)) {
    cros_window_state = ToChromeOsWindowState(window_state);
    out_window_info->window_state_type.emplace(cros_window_state);
  }

  std::string app_type;
  if (GetString(app, kAppType, &app_type) && app_type == kAppTypeArc) {
    FillArcExtraWindowInfoFromJson(app,
                                   &out_window_info->arc_extra_info.emplace());
  }

  const base::Value::Dict* window_bound = app.FindDict(kWindowBound);
  int top;
  int left;
  int width;
  int height;
  if (window_bound && GetInt(*window_bound, kWindowBoundTop, &top) &&
      GetInt(*window_bound, kWindowBoundLeft, &left) &&
      GetInt(*window_bound, kWindowBoundWidth, &width) &&
      GetInt(*window_bound, kWindowBoundHeight, &height)) {
    out_window_info->current_bounds.emplace(left, top, width, height);
  }

  int z_index;
  if (GetInt(app, kZIndex, &z_index))
    out_window_info->activation_index.emplace(z_index);

  std::string display_id_string;
  int64_t display_id;
  if (GetString(app, kDisplayId, &display_id_string) &&
      base::StringToInt64(display_id_string, &display_id)) {
    out_window_info->display_id = display_id;
  }

  std::string pre_minimized_window_state;
  if (GetString(app, kPreMinimizedWindowState, &pre_minimized_window_state) &&
      IsValidWindowState(pre_minimized_window_state) &&
      cros_window_state == chromeos::WindowStateType::kMinimized) {
    out_window_info->pre_minimized_show_state_type.emplace(
        ToUiWindowState(pre_minimized_window_state));
  }

  int snap_percentage;
  if (GetInt(app, kSnapPercentage, &snap_percentage))
    out_window_info->snap_percentage.emplace(snap_percentage);

  std::string title;
  if (GetString(app, kTitle, &title))
    out_window_info->app_title.emplace(base::UTF8ToUTF16(title));
}

// Convert a desk template to `app_restore::RestoreData`.
std::unique_ptr<app_restore::RestoreData> ConvertJsonToRestoreData(
    const base::Value::Dict* desk) {
  std::unique_ptr<app_restore::RestoreData> restore_data =
      std::make_unique<app_restore::RestoreData>();

  const base::Value::List* apps = desk->FindList(kApps);
  if (apps) {
    for (const auto& app : *apps) {
      const base::Value::Dict& app_dict = app.GetDict();
      std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
          ConvertJsonToAppLaunchInfo(app_dict);
      if (!app_launch_info)
        continue;  // Skip unsupported app.

      int window_id;
      if (!GetInt(app_dict, kWindowId, &window_id)) {
        return nullptr;
      }

      const std::string app_id = app_launch_info->app_id;
      restore_data->AddAppLaunchInfo(std::move(app_launch_info));

      app_restore::WindowInfo app_window_info;
      FillWindowInfoFromJson(app_dict, &app_window_info);

      restore_data->ModifyWindowInfo(app_id, window_id, app_window_info);
    }
  }

  return restore_data;
}

// Conversion to value methods.

base::Value::Dict ConvertWindowBoundToValue(const gfx::Rect& rect) {
  base::Value::Dict rectangle_value;

  rectangle_value.Set(kWindowBoundTop, base::Value(rect.y()));
  rectangle_value.Set(kWindowBoundLeft, base::Value(rect.x()));
  rectangle_value.Set(kWindowBoundHeight, base::Value(rect.height()));
  rectangle_value.Set(kWindowBoundWidth, base::Value(rect.width()));

  return rectangle_value;
}

base::Value::Dict ConvertSizeToValue(const gfx::Size& size) {
  base::Value::Dict size_value;

  size_value.Set(kSizeWidth, base::Value(size.width()));
  size_value.Set(kSizeHeight, base::Value(size.height()));

  return size_value;
}

// Convert ui::WindowStateType `window_state` to std::string used by the
// base::Value representation.
std::string ChromeOsWindowStateToString(
    const chromeos::WindowStateType& window_state) {
  switch (window_state) {
    case chromeos::WindowStateType::kNormal:
      return kWindowStateNormal;
    case chromeos::WindowStateType::kMinimized:
      return kWindowStateMinimized;
    case chromeos::WindowStateType::kMaximized:
      return kWindowStateMaximized;
    case chromeos::WindowStateType::kFullscreen:
      return kWindowStateFullscreen;
    case chromeos::WindowStateType::kPrimarySnapped:
      return kWindowStatePrimarySnapped;
    case chromeos::WindowStateType::kSecondarySnapped:
      return kWindowStateSecondarySnapped;
    case chromeos::WindowStateType::kFloated:
      return kWindowStateFloated;
    default:
      // Available states in JSON representation is a subset of all window
      // states enumerated by WindowStateType. Default to normal if not
      // supported.
      return kWindowStateNormal;
  }
}

// Convert ui::WindowShowState `state` to JSON used by the base::Value
// representation.
std::string UiWindowStateToString(const ui::WindowShowState& window_state) {
  switch (window_state) {
    case ui::WindowShowState::SHOW_STATE_NORMAL:
      return kWindowStateNormal;
    case ui::WindowShowState::SHOW_STATE_MINIMIZED:
      return kWindowStateMinimized;
    case ui::WindowShowState::SHOW_STATE_MAXIMIZED:
      return kWindowStateMaximized;
    case ui::WindowShowState::SHOW_STATE_FULLSCREEN:
      return kWindowStateFullscreen;
    default:
      // available states in JSON representation is a subset
      // of all window states enumerated by WindowShowState.
      // Default to normal if not supported.
      return kWindowStateNormal;
  }
}

// Returns a string WindowOpenDisposition when given a value of the
// WindowOpenDisposition passed into this function.  Assumes the caller
// casts the disposition from a int32_t.
std::string WindowOpenDispositionToString(WindowOpenDisposition disposition) {
  switch (disposition) {
    case WindowOpenDisposition::UNKNOWN:
      return kWindowOpenDispositionUnknown;
    case WindowOpenDisposition::CURRENT_TAB:
      return kWindowOpenDispositionCurrentTab;
    case WindowOpenDisposition::SINGLETON_TAB:
      return kWindowOpenDispositionSingletonTab;
    case WindowOpenDisposition::NEW_FOREGROUND_TAB:
      return kWindowOpenDispositionNewForegroundTab;
    case WindowOpenDisposition::NEW_BACKGROUND_TAB:
      return kWindowOpenDispositionNewBackgroundTab;
    case WindowOpenDisposition::NEW_POPUP:
      return kWindowOpenDispositionNewPopup;
    case WindowOpenDisposition::NEW_WINDOW:
      return kWindowOpenDispositionNewWindow;
    case WindowOpenDisposition::SAVE_TO_DISK:
      return kWindowOpenDispositionSaveToDisk;
    case WindowOpenDisposition::SWITCH_TO_TAB:
      return kWindowOpenDispositionSwitchToTab;
    case WindowOpenDisposition::OFF_THE_RECORD:
      return kWindowOpenDispositionOffTheRecord;
    case WindowOpenDisposition::IGNORE_ACTION:
      return kWindowOpenDispositionIgnoreAction;
    case WindowOpenDisposition::NEW_PICTURE_IN_PICTURE:
      return kWindowOpenDispositionNewPictureInPicture;
  }
}

std::string LaunchContainerToString(apps::LaunchContainer launch_container) {
  switch (launch_container) {
    case apps::LaunchContainer::kLaunchContainerWindow:
      return kLaunchContainerWindow;
    case apps::LaunchContainer::kLaunchContainerPanelDeprecated:
      return kLaunchContainerPanelDeprecated;
    case apps::LaunchContainer::kLaunchContainerTab:
      return kLaunchContainerTab;
    case apps::LaunchContainer::kLaunchContainerNone:
      return kLaunchContainerNone;
  }
}

base::Value::List ConvertURLsToBrowserAppTabValues(
    const std::vector<GURL>& urls) {
  base::Value::List tab_list;

  for (const auto& url : urls) {
    base::Value::Dict browser_tab;
    browser_tab.Set(kTabUrl, url.spec());
    tab_list.Append(std::move(browser_tab));
  }

  return tab_list;
}

std::string GetAppTypeForJson(apps::AppRegistryCache* apps_cache,
                              const std::string& app_id) {
  // This switch should follow the same structure as DeskSyncBridge#FillApp.
  switch (apps_cache->GetAppType(app_id)) {
    case apps::AppType::kWeb:
    case apps::AppType::kSystemWeb:
      return kAppTypeChrome;

    case apps::AppType::kChromeApp:
      if (app_id == app_constants::kChromeAppId) {
        return kAppTypeBrowser;
      } else {
        return kAppTypeChrome;
      }

    case apps::AppType::kStandaloneBrowser:
      if (app_id == app_constants::kLacrosAppId) {
        return kAppTypeBrowser;
      } else {
        return kAppTypeUnsupported;
      }

    case apps::AppType::kArc:
      return kAppTypeArc;

    case apps::AppType::kStandaloneBrowserChromeApp:
      return kAppTypeChrome;

    case apps::AppType::kUnknown:
      return kAppTypeUnknown;

    case apps::AppType::kBuiltIn:
    case apps::AppType::kCrostini:
    case apps::AppType::kPluginVm:
    case apps::AppType::kRemote:
    case apps::AppType::kBorealis:
    case apps::AppType::kBruschetta:
    case apps::AppType::kExtension:
    case apps::AppType::kStandaloneBrowserExtension:
      // Default to unsupported. This app should not be captured.
      return kAppTypeUnsupported;
  }
}

base::Value ConvertWindowToDeskApp(const std::string& app_id,
                                   const int window_id,
                                   const app_restore::AppRestoreData* app,
                                   apps::AppRegistryCache* apps_cache) {
  std::string app_type = GetAppTypeForJson(apps_cache, app_id);
  if (app_type == kAppTypeUnsupported) {
    return base::Value(base::Value::Type::NONE);
  }

  base::Value::Dict app_data;
  if (app_type != kAppTypeUnknown) {
    app_data.Set(kAppType, app_type);
  }

  if (app->window_info.current_bounds.has_value()) {
    app_data.Set(kWindowBound, ConvertWindowBoundToValue(
                                   app->window_info.current_bounds.value()));
  }

  const std::optional<app_restore::WindowInfo::ArcExtraInfo>& arc_info =
      app->window_info.arc_extra_info;
  if (arc_info) {
    if (arc_info->bounds_in_root.has_value()) {
      app_data.Set(kBoundsInRoot,
                   ConvertWindowBoundToValue(arc_info->bounds_in_root.value()));
    }

    if (arc_info->minimum_size.has_value()) {
      app_data.Set(kMinimumSize,
                   ConvertSizeToValue(arc_info->minimum_size.value()));
    }

    if (arc_info->maximum_size.has_value()) {
      app_data.Set(kMaximumSize,
                   ConvertSizeToValue(arc_info->maximum_size.value()));
    }
  }

  if (app->window_info.app_title.has_value()) {
    app_data.Set(kTitle, base::UTF16ToUTF8(app->window_info.app_title.value()));
  }

  chromeos::WindowStateType window_state = chromeos::WindowStateType::kDefault;
  if (app->window_info.window_state_type.has_value()) {
    window_state = app->window_info.window_state_type.value();
    app_data.Set(kWindowState, ChromeOsWindowStateToString(window_state));
  }

  // TODO(crbug.com/1311801): Add support for actual event_flag values.
  app_data.Set(kEventFlag, 0);

  if (app->window_info.activation_index.has_value()) {
    app_data.Set(kZIndex, app->window_info.activation_index.value());
  }

  if (!app->browser_extra_info.urls.empty()) {
    app_data.Set(
        kTabs, ConvertURLsToBrowserAppTabValues(app->browser_extra_info.urls));
  }

  if (!app->browser_extra_info.tab_group_infos.empty()) {
    base::Value::List tab_groups_value;

    for (const auto& tab_group : app->browser_extra_info.tab_group_infos) {
      tab_groups_value.Append(ConvertTabGroupInfoToDict(tab_group));
    }

    app_data.Set(kTabGroups, std::move(tab_groups_value));
  }

  if (app->browser_extra_info.active_tab_index.has_value()) {
    app_data.Set(kActiveTabIndex,
                 app->browser_extra_info.active_tab_index.value());
  }

  if (app->browser_extra_info.first_non_pinned_tab_index.has_value()) {
    app_data.Set(kFirstNonPinnedTabIndex,
                 app->browser_extra_info.first_non_pinned_tab_index.value());
  }

  if (app->browser_extra_info.app_type_browser.has_value()) {
    app_data.Set(kIsAppTypeBrowser,
                 app->browser_extra_info.app_type_browser.value());
  }

  app_data.Set(kAppId, app_id);

  app_data.Set(kWindowId, window_id);

  if (app->display_id.has_value()) {
    app_data.Set(kDisplayId, base::NumberToString(app->display_id.value()));
  }

  if (app->window_info.pre_minimized_show_state_type.has_value() &&
      window_state == chromeos::WindowStateType::kMinimized) {
    app_data.Set(kPreMinimizedWindowState,
                 UiWindowStateToString(
                     app->window_info.pre_minimized_show_state_type.value()));
  }

  if (app->window_info.snap_percentage.has_value()) {
    app_data.Set(kSnapPercentage,
                 static_cast<int>(app->window_info.snap_percentage.value()));
  }

  if (app->browser_extra_info.app_name.has_value()) {
    app_data.Set(kAppName, app->browser_extra_info.app_name.value());
  }

  if (app->disposition.has_value()) {
    WindowOpenDisposition disposition =
        static_cast<WindowOpenDisposition>(app->disposition.value());
    app_data.Set(kWindowOpenDisposition,
                 WindowOpenDispositionToString(disposition));
  }

  if (app->container.has_value()) {
    apps::LaunchContainer container =
        static_cast<apps::LaunchContainer>(app->container.value());
    app_data.Set(kLaunchContainer, LaunchContainerToString(container));
  }

  if (app->override_url.has_value()) {
    app_data.Set(kOverrideUrl, app->override_url->spec());
  }

  if (app->browser_extra_info.lacros_profile_id.has_value()) {
    app_data.Set(kLacrosProfileId,
                 base::NumberToString(
                     app->browser_extra_info.lacros_profile_id.value()));
  }

  return base::Value(std::move(app_data));
}

base::Value ConvertRestoreDataToValue(
    const app_restore::RestoreData* restore_data,
    apps::AppRegistryCache* apps_cache) {
  base::Value::List desk_data;

  for (const auto& app : restore_data->app_id_to_launch_list()) {
    for (const auto& window : app.second) {
      auto app_data = ConvertWindowToDeskApp(app.first, window.first,
                                             window.second.get(), apps_cache);
      if (app_data.is_none())
        continue;

      desk_data.Append(std::move(app_data));
    }
  }

  base::Value::Dict apps;
  apps.Set(kApps, std::move(desk_data));
  return base::Value(std::move(apps));
}

std::string SerializeDeskTypeAsString(ash::DeskTemplateType desk_type) {
  switch (desk_type) {
    case ash::DeskTemplateType::kTemplate:
      return kDeskTypeTemplate;
    case ash::DeskTemplateType::kSaveAndRecall:
      return kDeskTypeSaveAndRecall;
    case ash::DeskTemplateType::kFloatingWorkspace:
      return kDeskTypeFloatingWorkspace;
    case ash::DeskTemplateType::kUnknown:
      return kDeskTypeUnknown;
  }
}

bool IsValidDeskTemplateType(const std::string& desk_template_type) {
  return base::Contains(kValidDeskTypes, desk_template_type);
}

// TODO(b/258692868): Currently parse any invalid value for this field as
// SaveAndRecall. Fix by crash / signal some error instead.
ash::DeskTemplateType GetDeskTypeFromString(const std::string& desk_type) {
  DCHECK(IsValidDeskTemplateType(desk_type));
  if (desk_type == kDeskTypeTemplate)
    return ash::DeskTemplateType::kTemplate;
  else if (desk_type == kDeskTypeFloatingWorkspace)
    return ash::DeskTemplateType::kFloatingWorkspace;
  else if (desk_type == kDeskTypeSaveAndRecall)
    return ash::DeskTemplateType::kSaveAndRecall;
  else
    return ash::DeskTemplateType::kUnknown;
}

// Convert from apps::LaunchContainer to sync proto LaunchContainer.
// Assumes caller has cast `container` from int32_t to
// apps::LaunchContainer
SyncLaunchContainer FromLaunchContainer(apps::LaunchContainer container) {
  switch (container) {
    case apps::LaunchContainer::kLaunchContainerWindow:
      return sync_pb::
          WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_WINDOW;
    case apps::LaunchContainer::kLaunchContainerPanelDeprecated:
      return sync_pb::
          WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_PANEL_DEPRECATED;
    case apps::LaunchContainer::kLaunchContainerTab:
      return sync_pb::
          WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_TAB;
    case apps::LaunchContainer::kLaunchContainerNone:
      return sync_pb::
          WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_NONE;
  }
}

// Convert from sync proto LaunchContainer to apps::LaunchContainer.
// Assumes caller has cast `container` from int32_t to
// apps::LaunchContainer
apps::LaunchContainer ToLaunchContainer(SyncLaunchContainer container) {
  switch (container) {
    case sync_pb::
        WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_UNSPECIFIED:
      return apps::LaunchContainer::kLaunchContainerWindow;
    case sync_pb::
        WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_WINDOW:
      return apps::LaunchContainer::kLaunchContainerWindow;
    case sync_pb::
        WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_PANEL_DEPRECATED:
      return apps::LaunchContainer::kLaunchContainerPanelDeprecated;
    case sync_pb::WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_TAB:
      return apps::LaunchContainer::kLaunchContainerTab;
    case sync_pb::WorkspaceDeskSpecifics_LaunchContainer_LAUNCH_CONTAINER_NONE:
      return apps::LaunchContainer::kLaunchContainerNone;
  }
}

// Convert sync proto WindowOpenDisposition to base's WindowOpenDisposition.
// This value is cast to int32_t by the caller to be assigned to the
// `disposition` field in AppRestoreData.
WindowOpenDisposition ToBaseWindowOpenDisposition(
    SyncWindowOpenDisposition disposition) {
  switch (disposition) {
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_UNKNOWN:
      return WindowOpenDisposition::UNKNOWN;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_CURRENT_TAB:
      return WindowOpenDisposition::CURRENT_TAB;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_SINGLETON_TAB:
      return WindowOpenDisposition::SINGLETON_TAB;
    case sync_pb::
        WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_FOREGROUND_TAB:
      return WindowOpenDisposition::NEW_FOREGROUND_TAB;
    case sync_pb::
        WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_BACKGROUND_TAB:
      return WindowOpenDisposition::NEW_BACKGROUND_TAB;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_POPUP:
      return WindowOpenDisposition::NEW_POPUP;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_WINDOW:
      return WindowOpenDisposition::NEW_WINDOW;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_SAVE_TO_DISK:
      return WindowOpenDisposition::SAVE_TO_DISK;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_OFF_THE_RECORD:
      return WindowOpenDisposition::OFF_THE_RECORD;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_IGNORE_ACTION:
      return WindowOpenDisposition::IGNORE_ACTION;
    case sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_SWITCH_TO_TAB:
      return WindowOpenDisposition::SWITCH_TO_TAB;
    case sync_pb::
        WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_PICTURE_IN_PICTURE:
      return WindowOpenDisposition::NEW_PICTURE_IN_PICTURE;
  }
}

// Fill `out_gurls` using tabs' URL in `browser_app_window`.
void FillUrlList(const BrowserAppWindow& browser_app_window,
                 std::vector<GURL>* out_gurls) {
  for (auto tab : browser_app_window.tabs()) {
    if (tab.has_url())
      out_gurls->emplace_back(tab.url());
  }
}

// Since tab groups must have completely valid fields therefore this function
// exists to validate that sync tab groups are entirely valid.
bool ValidSyncTabGroup(const SyncTabGroup& sync_tab_group) {
  return sync_tab_group.has_first_index() && sync_tab_group.has_last_index() &&
         sync_tab_group.has_title() && sync_tab_group.has_color();
}

// Converts a sync tab group color to its tab_groups::TabGroupColorId
// equivalent.
TabGroupColor TabGroupColorIdFromSyncTabColor(
    const SyncTabGroupColor& sync_color) {
  switch (sync_color) {
    // Default to grey if unknown.
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_UNKNOWN_COLOR:
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREY:
      return TabGroupColor::kGrey;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_BLUE:
      return TabGroupColor::kBlue;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_RED:
      return TabGroupColor::kRed;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_YELLOW:
      return TabGroupColor::kYellow;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREEN:
      return TabGroupColor::kGreen;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PINK:
      return TabGroupColor::kPink;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PURPLE:
      return TabGroupColor::kPurple;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_CYAN:
      return TabGroupColor::kCyan;
    case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_ORANGE:
      return TabGroupColor::kOrange;
  };
}

// Instantiates a TabGroup from its sync equivalent.
tab_groups::TabGroupInfo FillTabGroupInfoFromProto(
    const SyncTabGroup& sync_tab_group) {
  // This function should never be called with a partially instantiated
  // tab group.
  DCHECK(ValidSyncTabGroup(sync_tab_group));

  return tab_groups::TabGroupInfo(
      {static_cast<uint32_t>(sync_tab_group.first_index()),
       static_cast<uint32_t>(sync_tab_group.last_index())},
      tab_groups::TabGroupVisualData(
          base::UTF8ToUTF16(sync_tab_group.title()),
          TabGroupColorIdFromSyncTabColor(sync_tab_group.color()),
          sync_tab_group.is_collapsed()));
}

// Fill `out_group_infos` using information found in the proto's
// tab group structure.
void FillTabGroupInfosFromProto(
    const BrowserAppWindow& browser_app_window,
    std::vector<tab_groups::TabGroupInfo>* out_group_infos) {
  for (const auto& group : browser_app_window.tab_groups()) {
    if (!ValidSyncTabGroup(group)) {
      continue;
    }

    out_group_infos->push_back(FillTabGroupInfoFromProto(group));
  }
}

// Get App ID from App proto.
std::string GetAppId(const sync_pb::WorkspaceDeskSpecifics_App& app) {
  switch (app.app().app_case()) {
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
      // Return an empty string to indicate this app is unsupported.
      return std::string();
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow: {
      const bool is_lacros =
#if BUILDFLAG(IS_CHROMEOS_LACROS)
          true;
#else
          // Note that this will launch the browser as lacros if it is enabled,
          // even if it was saved as a non-lacros window (and vice-versa).
          crosapi::lacros_startup_state::IsLacrosEnabled();
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

      // Browser app has a known app ID.
      return std::string(is_lacros ? app_constants::kLacrosAppId
                                   : app_constants::kChromeAppId);
    }
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
      return app.app().chrome_app().app_id();
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
      return app.app().progress_web_app().app_id();
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp:
      return app.app().arc_app().app_id();
  }
}

// Convert App proto to `app_restore::AppLaunchInfo`.
std::unique_ptr<app_restore::AppLaunchInfo> ConvertToAppLaunchInfo(
    const sync_pb::WorkspaceDeskSpecifics_App& app) {
  const std::string app_id = GetAppId(app);

  if (app_id.empty())
    return nullptr;

  auto app_launch_info =
      std::make_unique<app_restore::AppLaunchInfo>(app_id, app.window_id());

  if (app.has_display_id())
    app_launch_info->display_id = app.display_id();

  if (app.has_container()) {
    app_launch_info->container =
        static_cast<int32_t>(ToLaunchContainer(app.container()));
  }

  if (app.has_disposition()) {
    app_launch_info->disposition =
        static_cast<int32_t>(ToBaseWindowOpenDisposition(app.disposition()));
  }

  if (app.has_app_name()) {
    app_launch_info->browser_extra_info.app_name = app.app_name();
  }

  if (app.has_override_url()) {
    app_launch_info->override_url = GURL(app.override_url());
  }

  // This is a short-term fix as `event_flag` is required to launch ArcApp.
  // Currently we don't support persisting user action in template
  // so always default to 0 which is no action.
  // https://source.chromium.org/chromium/chromium/src/
  // +/main:ui/base/window_open_disposition.cc;l=34
  //
  // TODO(crbug.com/1311801): Add support for actual event_flag values.
  app_launch_info->event_flag = 0;

  switch (app.app().app_case()) {
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
      // This should never happen. `APP_NOT_SET` corresponds to empty `app_id`.
      // This method will early return when `app_id` is empty.
      NOTREACHED_IN_MIGRATION();
      break;
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow:
      if (app.app().browser_app_window().has_active_tab_index()) {
        app_launch_info->browser_extra_info.active_tab_index =
            app.app().browser_app_window().active_tab_index();
      }

      FillUrlList(app.app().browser_app_window(),
                  &app_launch_info->browser_extra_info.urls);

      if (app.app().browser_app_window().tab_groups_size() > 0) {
        FillTabGroupInfosFromProto(
            app.app().browser_app_window(),
            &app_launch_info->browser_extra_info.tab_group_infos);
      }

      if (app.app().browser_app_window().has_show_as_app()) {
        app_launch_info->browser_extra_info.app_type_browser =
            app.app().browser_app_window().show_as_app();
      }

      if (app.app().browser_app_window().has_first_non_pinned_tab_index()) {
        app_launch_info->browser_extra_info.first_non_pinned_tab_index =
            app.app().browser_app_window().first_non_pinned_tab_index();
      }

      break;
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
      // `app_id` is enough to identify a Chrome app.
      break;
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
      // `app_id` is enough to identify a Progressive Web app.
      break;
    case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp:
      // `app_id` is enough to identify an Arc app.
      break;
  }

  return app_launch_info;
}

// Convert sync proto WindowOpenDisposition to base's WindowOpenDisposition.
// This value is cast to int32_t by the caller to be assigned to the
// `disposition` field in AppRestoreData.
SyncWindowOpenDisposition FromBaseWindowOpenDisposition(
    WindowOpenDisposition disposition) {
  switch (disposition) {
    case WindowOpenDisposition::UNKNOWN:
      return sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_UNKNOWN;
    case WindowOpenDisposition::CURRENT_TAB:
      return sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_CURRENT_TAB;
    case WindowOpenDisposition::SINGLETON_TAB:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_SINGLETON_TAB;
    case WindowOpenDisposition::NEW_FOREGROUND_TAB:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_FOREGROUND_TAB;
    case WindowOpenDisposition::NEW_BACKGROUND_TAB:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_BACKGROUND_TAB;
    case WindowOpenDisposition::NEW_POPUP:
      return sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_POPUP;
    case WindowOpenDisposition::NEW_WINDOW:
      return sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_WINDOW;
    case WindowOpenDisposition::SAVE_TO_DISK:
      return sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition_SAVE_TO_DISK;
    case WindowOpenDisposition::OFF_THE_RECORD:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_OFF_THE_RECORD;
    case WindowOpenDisposition::IGNORE_ACTION:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_IGNORE_ACTION;
    case WindowOpenDisposition::SWITCH_TO_TAB:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_SWITCH_TO_TAB;
    case WindowOpenDisposition::NEW_PICTURE_IN_PICTURE:
      return sync_pb::
          WorkspaceDeskSpecifics_WindowOpenDisposition_NEW_PICTURE_IN_PICTURE;
  }
}

// Convert Sync proto WindowState `state` to ui::WindowShowState used by
// the app_restore::WindowInfo struct.
ui::WindowShowState ToUiWindowState(WindowState state) {
  switch (state) {
    case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
      return ui::WindowShowState::SHOW_STATE_NORMAL;
    case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
      return ui::WindowShowState::SHOW_STATE_NORMAL;
    case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
      return ui::WindowShowState::SHOW_STATE_MINIMIZED;
    case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
      return ui::WindowShowState::SHOW_STATE_MAXIMIZED;
    case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
      return ui::WindowShowState::SHOW_STATE_FULLSCREEN;
    case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
      return ui::WindowShowState::SHOW_STATE_NORMAL;
    case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
      return ui::WindowShowState::SHOW_STATE_NORMAL;
    case WindowState::WorkspaceDeskSpecifics_WindowState_FLOATED:
      return ui::WindowShowState::SHOW_STATE_NORMAL;
  }
}

// Convert Sync proto WindowState `state` to chromeos::WindowStateType used
// by the app_restore::WindowInfo struct.
chromeos::WindowStateType ToChromeOsWindowState(WindowState state) {
  switch (state) {
    case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
      return chromeos::WindowStateType::kNormal;
    case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
      return chromeos::WindowStateType::kNormal;
    case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
      return chromeos::WindowStateType::kMinimized;
    case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
      return chromeos::WindowStateType::kMaximized;
    case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
      return chromeos::WindowStateType::kFullscreen;
    case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
      return chromeos::WindowStateType::kPrimarySnapped;
    case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
      return chromeos::WindowStateType::kSecondarySnapped;
    case WindowState::WorkspaceDeskSpecifics_WindowState_FLOATED:
      return chromeos::WindowStateType::kFloated;
  }
}

// Convert chromeos::WindowStateType to Sync proto WindowState.
WindowState FromChromeOsWindowState(chromeos::WindowStateType state) {
  switch (state) {
    case chromeos::WindowStateType::kDefault:
    case chromeos::WindowStateType::kNormal:
    case chromeos::WindowStateType::kInactive:
    case chromeos::WindowStateType::kPinned:
    case chromeos::WindowStateType::kTrustedPinned:
    case chromeos::WindowStateType::kPip:
      // TODO(crbug.com/1331825): Float state support for desk template.
      return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
    case chromeos::WindowStateType::kMinimized:
      return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
    case chromeos::WindowStateType::kMaximized:
      return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
    case chromeos::WindowStateType::kFullscreen:
      return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
    case chromeos::WindowStateType::kPrimarySnapped:
      return WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED;
    case chromeos::WindowStateType::kSecondarySnapped:
      return WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED;
    case chromeos::WindowStateType::kFloated:
      return WindowState::WorkspaceDeskSpecifics_WindowState_FLOATED;
  }
}

// Convert ui::WindowShowState to Sync proto WindowState.
WindowState FromUiWindowState(ui::WindowShowState state) {
  switch (state) {
    case ui::WindowShowState::SHOW_STATE_DEFAULT:
    case ui::WindowShowState::SHOW_STATE_NORMAL:
    case ui::WindowShowState::SHOW_STATE_INACTIVE:
    case ui::WindowShowState::SHOW_STATE_END:
      return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
    case ui::WindowShowState::SHOW_STATE_MINIMIZED:
      return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
    case ui::WindowShowState::SHOW_STATE_MAXIMIZED:
      return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
    case ui::WindowShowState::SHOW_STATE_FULLSCREEN:
      return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
  }
}

// Converts a sync tab group color to its tab_groups::TabGroupColorId
// equivalent.
SyncTabGroupColor SyncTabColorFromTabGroupColorId(
    const TabGroupColor& sync_color) {
  switch (sync_color) {
    case TabGroupColor::kGrey:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREY;
    case TabGroupColor::kBlue:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_BLUE;
    case TabGroupColor::kRed:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_RED;
    case TabGroupColor::kYellow:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_YELLOW;
    case TabGroupColor::kGreen:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREEN;
    case TabGroupColor::kPink:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PINK;
    case TabGroupColor::kPurple:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PURPLE;
    case TabGroupColor::kCyan:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_CYAN;
    case TabGroupColor::kOrange:
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_ORANGE;
    case TabGroupColor::kNumEntries:
      NOTREACHED_IN_MIGRATION() << "kNumEntries is not a supported color enum.";
      return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREY;
  };
}

void FillSyncTabGroupInfo(const tab_groups::TabGroupInfo& tab_group_info,
                          SyncTabGroup* out_sync_tab_group) {
  out_sync_tab_group->set_first_index(tab_group_info.tab_range.start());
  out_sync_tab_group->set_last_index(tab_group_info.tab_range.end());
  out_sync_tab_group->set_title(
      base::UTF16ToUTF8(tab_group_info.visual_data.title()));
  // Save some storage space by leaving is_collapsed to default value if the
  // tab group isn't collapsed.
  if (tab_group_info.visual_data.is_collapsed()) {
    out_sync_tab_group->set_is_collapsed(
        tab_group_info.visual_data.is_collapsed());
  }
  out_sync_tab_group->set_color(
      SyncTabColorFromTabGroupColorId(tab_group_info.visual_data.color()));
}

void FillBrowserAppTabGroupInfos(
    const std::vector<tab_groups::TabGroupInfo>& tab_group_infos,
    BrowserAppWindow* out_browser_app_window) {
  for (const auto& tab_group : tab_group_infos) {
    SyncTabGroup* sync_tab_group = out_browser_app_window->add_tab_groups();
    FillSyncTabGroupInfo(tab_group, sync_tab_group);
  }
}

// Fill `out_browser_app_window` with the given GURLs as BrowserAppTabs.
void FillBrowserAppTabs(const std::vector<GURL>& gurls,
                        BrowserAppWindow* out_browser_app_window) {
  for (const auto& gurl : gurls) {
    const std::string& url = gurl.spec();
    if (url.empty()) {
      // Skip invalid URLs.
      continue;
    }
    BrowserAppTab* browser_app_tab = out_browser_app_window->add_tabs();
    browser_app_tab->set_url(url);
  }
}

// Fill `out_browser_app_window` with urls and tab information from
// `app_restore_data`.
void FillBrowserAppWindow(const app_restore::AppRestoreData* app_restore_data,
                          BrowserAppWindow* out_browser_app_window) {
  const app_restore::BrowserExtraInfo browser_extra_info =
      app_restore_data->browser_extra_info;
  if (!browser_extra_info.urls.empty()) {
    FillBrowserAppTabs(browser_extra_info.urls, out_browser_app_window);
  }

  if (browser_extra_info.active_tab_index.has_value()) {
    out_browser_app_window->set_active_tab_index(
        browser_extra_info.active_tab_index.value());
  }

  if (browser_extra_info.app_type_browser.has_value()) {
    out_browser_app_window->set_show_as_app(
        browser_extra_info.app_type_browser.value());
  }

  if (!browser_extra_info.tab_group_infos.empty()) {
    FillBrowserAppTabGroupInfos(browser_extra_info.tab_group_infos,
                                out_browser_app_window);
  }

  if (browser_extra_info.first_non_pinned_tab_index.has_value()) {
    out_browser_app_window->set_first_non_pinned_tab_index(
        browser_extra_info.first_non_pinned_tab_index.value());
  }
}

// Fill `out_window_bounds` with information from `bounds`.
void FillWindowBound(const gfx::Rect& bounds, WindowBound* out_window_bounds) {
  out_window_bounds->set_left(bounds.x());
  out_window_bounds->set_top(bounds.y());
  out_window_bounds->set_width(bounds.width());
  out_window_bounds->set_height(bounds.height());
}

// Fill `out_app` with information from `window_info`.
void FillAppWithWindowInfo(const app_restore::WindowInfo* window_info,
                           WorkspaceDeskSpecifics_App* out_app) {
  if (window_info->activation_index.has_value())
    out_app->set_z_index(window_info->activation_index.value());

  if (window_info->current_bounds.has_value()) {
    FillWindowBound(window_info->current_bounds.value(),
                    out_app->mutable_window_bound());
  }

  if (window_info->window_state_type.has_value()) {
    out_app->set_window_state(
        FromChromeOsWindowState(window_info->window_state_type.value()));
  }

  if (window_info->pre_minimized_show_state_type.has_value()) {
    out_app->set_pre_minimized_window_state(
        FromUiWindowState(window_info->pre_minimized_show_state_type.value()));
  }

  if (window_info->snap_percentage.has_value())
    out_app->set_snap_percentage(window_info->snap_percentage.value());

  if (window_info->app_title.has_value())
    out_app->set_title(base::UTF16ToUTF8(window_info->app_title.value()));

  // AppRestoreData.GetWindowInfo does not include `display_id` in the returned
  // WindowInfo. Therefore, we are not filling `display_id` here.
}

//  Fill `out_app` with the `display_id` from `app_restore_data`.
void FillAppWithDisplayId(const app_restore::AppRestoreData* app_restore_data,
                          WorkspaceDeskSpecifics_App* out_app) {
  if (app_restore_data->display_id.has_value())
    out_app->set_display_id(app_restore_data->display_id.value());
}

//  Fill `out_app` with `container` from `app_restore_data`.
void FillAppWithLaunchContainer(
    const app_restore::AppRestoreData* app_restore_data,
    WorkspaceDeskSpecifics_App* out_app) {
  if (app_restore_data->container.has_value()) {
    out_app->set_container(
        FromLaunchContainer(static_cast<apps::LaunchContainer>(
            app_restore_data->container.value())));
  }
}

// Fill `out_app` with `disposition` from `app_restore_data`.
void FillAppWithWindowOpenDisposition(
    const app_restore::AppRestoreData* app_restore_data,
    WorkspaceDeskSpecifics_App* out_app) {
  if (app_restore_data->disposition.has_value()) {
    out_app->set_disposition(
        FromBaseWindowOpenDisposition(static_cast<WindowOpenDisposition>(
            app_restore_data->disposition.value())));
  }
}

// Fills `out_app` with `app_name` and `title` from `app_restore_data`.
void FillAppWithAppNameAndTitle(
    const app_restore::AppRestoreData* app_restore_data,
    WorkspaceDeskSpecifics_App* out_app) {
  const std::string app_name =
      app_restore_data->browser_extra_info.app_name.value_or("");
  if (!app_name.empty()) {
    out_app->set_app_name(app_name);
  }

  if (app_restore_data->window_info.app_title.has_value() &&
      !app_restore_data->window_info.app_title->empty()) {
    out_app->set_title(
        base::UTF16ToUTF8(*app_restore_data->window_info.app_title));
  }
}

void FillAppWithAppOverrideUrl(
    const app_restore::AppRestoreData* app_restore_data,
    WorkspaceDeskSpecifics_App* out_app) {
  if (app_restore_data->override_url.has_value()) {
    out_app->set_override_url(app_restore_data->override_url->spec());
  }
}

void FillArcAppSize(const gfx::Size& size, ArcAppWindowSize* out_window_size) {
  out_window_size->set_width(size.width());
  out_window_size->set_height(size.height());
}

void FillArcBoundsInRoot(const gfx::Rect& data_rect, WindowBound* out_rect) {
  out_rect->set_left(data_rect.x());
  out_rect->set_top(data_rect.y());
  out_rect->set_width(data_rect.width());
  out_rect->set_height(data_rect.height());
}

void FillArcApp(const app_restore::AppRestoreData* app_restore_data,
                ArcApp* out_app) {
  const std::optional<app_restore::WindowInfo::ArcExtraInfo>& arc_info =
      app_restore_data->window_info.arc_extra_info;
  if (!arc_info) {
    return;
  }

  if (arc_info->minimum_size.has_value()) {
    FillArcAppSize(arc_info->minimum_size.value(),
                   out_app->mutable_minimum_size());
  }
  if (arc_info->maximum_size.has_value()) {
    FillArcAppSize(arc_info->maximum_size.value(),
                   out_app->mutable_maximum_size());
  }
  if (arc_info->bounds_in_root.has_value()) {
    FillArcBoundsInRoot(arc_info->bounds_in_root.value(),
                        out_app->mutable_bounds_in_root());
  }
}

// Fills an app with container and open disposition.  This is only done in the
// specific cases of Chrome Apps and PWAs.
void FillAppWithLaunchContainerAndOpenDisposition(
    const app_restore::AppRestoreData* app_restore_data,
    WorkspaceDeskSpecifics_App* out_app) {
  // If present, fills the proto's `container` field with the information stored
  // in the `app_restore_data`'s `container` field.
  FillAppWithLaunchContainer(app_restore_data, out_app);

  // If present, fills the proto's `disposition` field with the information
  // stored in the `app_restore_data`'s `disposition` field.
  FillAppWithWindowOpenDisposition(app_restore_data, out_app);
}

// Fill `out_app` with `app_restore_data`.
// Return `false` if app type is unsupported.
bool FillApp(const std::string& app_id,
             const apps::AppType app_type,
             const app_restore::AppRestoreData* app_restore_data,
             WorkspaceDeskSpecifics_App* out_app) {
  // See definition in components/services/app_service/public/cpp/app_types.h
  switch (app_type) {
    case apps::AppType::kWeb:
    case apps::AppType::kSystemWeb: {
      // System Web Apps.
      // kSystemWeb is returned for System Web Apps in Lacros-primary
      // configuration. These can be persisted and launched the same way as
      // Chrome Apps.
      ChromeApp* chrome_app_window =
          out_app->mutable_app()->mutable_chrome_app();
      chrome_app_window->set_app_id(app_id);
      FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
      break;
    }

    case apps::AppType::kChromeApp: {
      // Ash Chrome browser OR PWA OR Chrome App hosted in Ash Chrome.
      if (app_constants::kChromeAppId == app_id) {
        // This window is either a browser window or a PWA window.
        // Both cases are persisted as "browser app" since they are launched the
        // same way. PWA window will have field `app_name` and
        // `app_type_browser` fields set. FillAppWithAppNameAndTitle has
        // persisted `app_name` field. FillBrowserAppWindow will persist
        // `app_type_browser` field.
        BrowserAppWindow* browser_app_window =
            out_app->mutable_app()->mutable_browser_app_window();
        FillBrowserAppWindow(app_restore_data, browser_app_window);
      } else {
        // Chrome App
        ChromeApp* chrome_app_window =
            out_app->mutable_app()->mutable_chrome_app();
        chrome_app_window->set_app_id(app_id);
        FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
      }
      break;
    }

    case apps::AppType::kStandaloneBrowser: {
      if (app_constants::kLacrosAppId == app_id) {
        // Lacros Chrome browser window or PWA hosted in Lacros Chrome.
        BrowserAppWindow* browser_app_window =
            out_app->mutable_app()->mutable_browser_app_window();
        FillBrowserAppWindow(app_restore_data, browser_app_window);
      } else {
        // Chrome app running in Lacros should have
        // AppType::kStandaloneBrowserChromeApp and never reach here.
        NOTREACHED_IN_MIGRATION();
        // Ignore this app type.
        return false;
      }

      break;
    }

    case apps::AppType::kStandaloneBrowserChromeApp: {
      // Chrome App hosted in Lacros.
      ChromeApp* chrome_app_window =
          out_app->mutable_app()->mutable_chrome_app();
      chrome_app_window->set_app_id(app_id);
      FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
      break;
    }

    case apps::AppType::kArc: {
      ArcApp* arc_app = out_app->mutable_app()->mutable_arc_app();
      arc_app->set_app_id(app_id);
      FillArcApp(app_restore_data, arc_app);
      break;
    }

    case apps::AppType::kBuiltIn:
    case apps::AppType::kCrostini:
    case apps::AppType::kPluginVm:
    case apps::AppType::kUnknown:
    case apps::AppType::kRemote:
    case apps::AppType::kBorealis:
    case apps::AppType::kBruschetta:
    case apps::AppType::kExtension:
    case apps::AppType::kStandaloneBrowserExtension:
      // Unsupported app types will be ignored.
      return false;
  }

  FillAppWithWindowInfo(app_restore_data->GetWindowInfo().get(), out_app);

  // AppRestoreData.GetWindowInfo does not include `display_id` in the returned
  // WindowInfo. We need to fill the `display_id` from AppRestoreData.
  FillAppWithDisplayId(app_restore_data, out_app);

  // If present, fills the proto's `app_name` and `title` fields with the
  // information stored in the `app_restore_data`'s `app_name` and `title`
  // fields.
  FillAppWithAppNameAndTitle(app_restore_data, out_app);

  // If present, fills the proto's `override_url` field with the information
  // from `app_restore_data`.
  FillAppWithAppOverrideUrl(app_restore_data, out_app);

  return true;
}

void FillArcExtraInfoFromProto(const ArcApp& app,
                               app_restore::WindowInfo* out_window_info) {
  out_window_info->arc_extra_info.emplace();
  app_restore::WindowInfo::ArcExtraInfo& arc_info =
      out_window_info->arc_extra_info.value();
  if (app.has_minimum_size()) {
    arc_info.minimum_size.emplace(app.minimum_size().width(),
                                  app.minimum_size().height());
  }
  if (app.has_maximum_size()) {
    arc_info.maximum_size.emplace(app.maximum_size().width(),
                                  app.maximum_size().height());
  }

  if (app.has_bounds_in_root()) {
    arc_info.bounds_in_root.emplace(
        app.bounds_in_root().left(), app.bounds_in_root().top(),
        app.bounds_in_root().width(), app.bounds_in_root().height());
  }
}

// Fill `out_window_info` with information from Sync proto `app`.
void FillWindowInfoFromProto(sync_pb::WorkspaceDeskSpecifics_App& app,
                             app_restore::WindowInfo* out_window_info) {
  if (app.has_window_state() &&
      sync_pb::WorkspaceDeskSpecifics_WindowState_IsValid(app.window_state())) {
    out_window_info->window_state_type.emplace(
        ToChromeOsWindowState(app.window_state()));
  }

  if (app.has_window_bound()) {
    out_window_info->current_bounds.emplace(
        app.window_bound().left(), app.window_bound().top(),
        app.window_bound().width(), app.window_bound().height());
  }

  if (app.has_z_index())
    out_window_info->activation_index.emplace(app.z_index());

  if (app.has_display_id())
    out_window_info->display_id.emplace(app.display_id());

  if (app.has_pre_minimized_window_state() &&
      app.window_state() ==
          sync_pb::WorkspaceDeskSpecifics_WindowState_MINIMIZED) {
    out_window_info->pre_minimized_show_state_type.emplace(
        ToUiWindowState(app.pre_minimized_window_state()));
  }

  if (app.has_snap_percentage() &&
      (app.window_state() ==
           sync_pb::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED ||
       app.window_state() ==
           sync_pb::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED)) {
    out_window_info->snap_percentage.emplace(app.snap_percentage());
  }

  if (app.has_title())
    out_window_info->app_title.emplace(base::UTF8ToUTF16(app.title()));

  if (app.app().app_case() ==
      sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp) {
    FillArcExtraInfoFromProto(app.app().arc_app(), out_window_info);
  }
}

// Convert a desk template to `app_restore::RestoreData`.
std::unique_ptr<app_restore::RestoreData> ConvertToRestoreData(
    const sync_pb::WorkspaceDeskSpecifics& entry_proto) {
  auto restore_data = std::make_unique<app_restore::RestoreData>();

  for (auto app_proto : entry_proto.desk().apps()) {
    std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
        ConvertToAppLaunchInfo(app_proto);
    if (!app_launch_info) {
      // Skip unsupported app.
      continue;
    }

    const std::string app_id = app_launch_info->app_id;
    restore_data->AddAppLaunchInfo(std::move(app_launch_info));

    app_restore::WindowInfo app_window_info;
    FillWindowInfoFromProto(app_proto, &app_window_info);

    restore_data->ModifyWindowInfo(app_id, app_proto.window_id(),
                                   app_window_info);
  }

  return restore_data;
}

// Fill a desk template `out_entry_proto` with information from
// `restore_data`.
void FillWorkspaceDeskSpecifics(
    apps::AppRegistryCache* apps_cache,
    const app_restore::RestoreData* restore_data,
    sync_pb::WorkspaceDeskSpecifics* out_entry_proto) {
  DCHECK(apps_cache);

  for (auto const& app_id_to_launch_list :
       restore_data->app_id_to_launch_list()) {
    const std::string app_id = app_id_to_launch_list.first;

    for (auto const& window_id_to_launch_info : app_id_to_launch_list.second) {
      const int window_id = window_id_to_launch_info.first;
      const app_restore::AppRestoreData* app_restore_data =
          window_id_to_launch_info.second.get();

      const auto app_type = apps_cache->GetAppType(app_id);

      WorkspaceDeskSpecifics_App* app =
          out_entry_proto->mutable_desk()->add_apps();
      app->set_window_id(window_id);
      if (!FillApp(app_id, app_type, app_restore_data, app)) {
        // Unsupported app type, remove this app entry.
        out_entry_proto->mutable_desk()->mutable_apps()->RemoveLast();
      }
    }
  }
}

// Fill a desk template `out_entry_proto` with the type of desk based on the
// desk's type field.
void FillDeskType(const DeskTemplate* desk_template,
                  sync_pb::WorkspaceDeskSpecifics* out_entry_proto) {
  switch (desk_template->type()) {
    case DeskTemplateType::kTemplate:
      out_entry_proto->set_desk_type(
          SyncDeskType::WorkspaceDeskSpecifics_DeskType_TEMPLATE);
      return;
    case DeskTemplateType::kSaveAndRecall:
      out_entry_proto->set_desk_type(
          SyncDeskType::WorkspaceDeskSpecifics_DeskType_SAVE_AND_RECALL);
      return;
    case DeskTemplateType::kFloatingWorkspace:
      out_entry_proto->set_desk_type(
          SyncDeskType::WorkspaceDeskSpecifics_DeskType_FLOATING_WORKSPACE);
      return;
    // Do nothing if type is unknown.
    case DeskTemplateType::kUnknown:
      return;
  }
}

// Takes in the Proto enum for a desk type `proto_type` and returns it's
// DeskTemplateType equivalent.
DeskTemplateType GetDeskTemplateTypeFromProtoType(
    const SyncDeskType& proto_type) {
  switch (proto_type) {
    // Treat unknown desk types as templates.
    case SyncDeskType::WorkspaceDeskSpecifics_DeskType_UNKNOWN_TYPE:
      return DeskTemplateType::kUnknown;
    case SyncDeskType::WorkspaceDeskSpecifics_DeskType_TEMPLATE:
      return DeskTemplateType::kTemplate;
    case SyncDeskType::WorkspaceDeskSpecifics_DeskType_SAVE_AND_RECALL:
      return DeskTemplateType::kSaveAndRecall;
    case SyncDeskType::WorkspaceDeskSpecifics_DeskType_FLOATING_WORKSPACE:
      return DeskTemplateType::kFloatingWorkspace;
  }
}

// Corrects the admin template browser format so that subsequent serialization
// code stores browsers correctly.
void CorrectAdminTemplateBrowserFormat(base::Value& app) {
  if (!app.is_dict()) {
    return;
  }

  auto& app_dict = app.GetDict();
  base::Value::List* tabs = app_dict.FindList(kTabsAdminFormat);

  if (tabs == nullptr) {
    return;
  }

  app_dict.Set(kTabs, tabs->Clone());
}

// Corrects the admin template format for app types.  Modifies the reference
// passed.
void CorrectAdminTemplateAppTypeFormat(base::Value& app) {
  if (!app.is_dict()) {
    return;
  }

  auto& app_dict = app.GetDict();

  std::string app_type;
  if (!GetString(app_dict, kAppType, &app_type)) {
    return;
  }

  // In the future all these types will be supported so we include them here
  // to exhaust the possible enum types that can be given to us.  However
  // app types that are not browser will not be supported in the current version
  // of admin templates so return unsupported for everything other than
  // browsers.
  if (app_type == kAppTypeBrowserAdminFormat) {
    app_dict.Set(kAppType, kAppTypeBrowser);
    CorrectAdminTemplateBrowserFormat(app);
  } else if (app_type == kAppTypeArcAdminFormat) {
    app_dict.Set(kAppType, kAppTypeUnsupported);
  } else if (app_type == kAppTypeChromeAdminFormat) {
    app_dict.Set(kAppType, kAppTypeUnsupported);
  } else if (app_type == kAppTypeProgressiveWebAdminFormat) {
    app_dict.Set(kAppType, kAppTypeUnsupported);
  } else if (app_type == kAppTypeIsolatedWebAppAdminFormat) {
    app_dict.Set(kAppType, kAppTypeUnsupported);
  } else {
    app_dict.Set(kAppType, kAppTypeUnsupported);
  }
}

// Modifies the strings in the desk's apps such that they match the format
// defined by this file.  This does not verify the format, that is handled
// by `ConvertJsonToRestoreData`.  The value is copied and returned corrected.
// If the admin format itself is corrupted return the clone, it will be
// discarded by the parsing code.
base::Value::Dict CorrectAdminTemplateFormat(const base::Value::Dict* desk) {
  auto desk_clone = desk->Clone();
  base::Value::List* apps = desk_clone.FindList(kApps);
  if (apps == nullptr) {
    return desk_clone;
  }

  if (apps) {
    for (auto& app : *apps) {
      CorrectAdminTemplateAppTypeFormat(app);
    }
  }

  return desk_clone;
}

std::unique_ptr<ash::DeskTemplate> ParseAdminTemplate(
    const base::Value& admin_template) {
  if (!admin_template.is_dict()) {
    return nullptr;
  }

  const base::Value::Dict& value_dict = admin_template.GetDict();

  bool auto_launch_on_startup;
  std::string created_time_usec_str;
  int64_t created_time_usec;
  std::string name;
  std::string updated_time_usec_str;
  int64_t updated_time_usec;
  std::string uuid_str;
  const base::Value::Dict* desk = value_dict.FindDict(kDesk);
  if (!desk ||
      !GetBool(value_dict, kAutoLaunchOnStartup, &auto_launch_on_startup) ||
      !GetString(value_dict, kUuid, &uuid_str) ||
      !GetString(value_dict, kName, &name) ||
      !GetString(value_dict, kCreatedTime, &created_time_usec_str) ||
      !base::StringToInt64(created_time_usec_str, &created_time_usec) ||
      !GetString(value_dict, kUpdatedTime, &updated_time_usec_str) ||
      !base::StringToInt64(updated_time_usec_str, &updated_time_usec) ||
      name.empty() || created_time_usec_str.empty() ||
      updated_time_usec_str.empty()) {
    return nullptr;
  }

  base::Uuid uuid = base::Uuid::ParseCaseInsensitive(uuid_str);
  if (!uuid.is_valid()) {
    return nullptr;
  }

  const base::Time created_time =
      desks_storage::desk_template_conversion::ProtoTimeToTime(
          created_time_usec);
  const base::Time updated_time =
      desks_storage::desk_template_conversion::ProtoTimeToTime(
          updated_time_usec);

  auto ash_admin_template = std::make_unique<ash::DeskTemplate>(
      std::move(uuid), ash::DeskTemplateSource::kPolicy, name, created_time,
      ash::DeskTemplateType::kTemplate, auto_launch_on_startup,
      admin_template.Clone());

  auto corrected_desk = CorrectAdminTemplateFormat(desk);
  ash_admin_template->set_updated_time(updated_time);
  ash_admin_template->set_desk_restore_data(
      ConvertJsonToRestoreData(&corrected_desk));

  return ash_admin_template;
}

}  // namespace

namespace desks_storage {

namespace desk_template_conversion {

// Converts the TabGroupColorId passed into its string equivalent
// as defined in the k constants above.
std::string ConvertTabGroupColorIdToString(GroupColor color) {
  switch (color) {
    case GroupColor::kGrey:
      return tab_groups::kTabGroupColorGrey;
    case GroupColor::kBlue:
      return tab_groups::kTabGroupColorBlue;
    case GroupColor::kRed:
      return tab_groups::kTabGroupColorRed;
    case GroupColor::kYellow:
      return tab_groups::kTabGroupColorYellow;
    case GroupColor::kGreen:
      return tab_groups::kTabGroupColorGreen;
    case GroupColor::kPink:
      return tab_groups::kTabGroupColorPink;
    case GroupColor::kPurple:
      return tab_groups::kTabGroupColorPurple;
    case GroupColor::kCyan:
      return tab_groups::kTabGroupColorCyan;
    case GroupColor::kOrange:
      return tab_groups::kTabGroupColorOrange;
    case GroupColor::kNumEntries:
      NOTREACHED_IN_MIGRATION() << "kNumEntries is not a supported color enum.";
      return tab_groups::kTabGroupColorGrey;
  }
}

// Converts a time field from sync protobufs to a time object.
base::Time ProtoTimeToTime(int64_t proto_time) {
  return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(proto_time));
}

// Converts a time object to the format used in sync protobufs
// (Microseconds since the Windows epoch).
int64_t TimeToProtoTime(const base::Time& t) {
  return t.ToDeltaSinceWindowsEpoch().InMicroseconds();
}

std::vector<std::unique_ptr<ash::DeskTemplate>>
ParseAdminTemplatesFromPolicyValue(const base::Value& value) {
  std::vector<std::unique_ptr<ash::DeskTemplate>> desk_templates;
  if (!value.is_list()) {
    return desk_templates;
  }

  for (const auto& desk_template : value.GetList()) {
    auto desk_template_ptr = ParseAdminTemplate(desk_template);
    if (desk_template_ptr == nullptr) {
      continue;
    }

    desk_templates.push_back(std::move(desk_template_ptr));
  }

  return desk_templates;
}

ParseSavedDeskResult ParseDeskTemplateFromBaseValue(
    const base::Value& value,
    ash::DeskTemplateSource source) {
  if (!value.is_dict()) {
    return base::unexpected(SavedDeskParseError::kBaseValueIsNotDict);
  }

  const base::Value::Dict& value_dict = value.GetDict();

  std::string created_time_usec_str;
  int64_t created_time_usec;
  std::string name;
  std::string updated_time_usec_str;
  int64_t updated_time_usec;
  std::string uuid_str;
  int version;
  const base::Value::Dict* desk = value_dict.FindDict(kDesk);
  if (!desk || !GetInt(value_dict, kVersion, &version) ||
      !GetString(value_dict, kUuid, &uuid_str) ||
      !GetString(value_dict, kName, &name) ||
      !GetString(value_dict, kCreatedTime, &created_time_usec_str) ||
      !base::StringToInt64(created_time_usec_str, &created_time_usec) ||
      !GetString(value_dict, kUpdatedTime, &updated_time_usec_str) ||
      !base::StringToInt64(updated_time_usec_str, &updated_time_usec) ||
      name.empty() || created_time_usec_str.empty() ||
      updated_time_usec_str.empty()) {
    return base::unexpected(SavedDeskParseError::kMissingRequiredFields);
  }

  base::Uuid uuid = base::Uuid::ParseCaseInsensitive(uuid_str);
  if (!uuid.is_valid()) {
    return base::unexpected(SavedDeskParseError::kInvalidUuid);
  }

  // Set default value for the desk type to template.
  std::string desk_type_string;
  if (!GetString(value_dict, kDeskType, &desk_type_string)) {
    desk_type_string = kDeskTypeTemplate;
  } else if (!IsValidDeskTemplateType(desk_type_string)) {
    return base::unexpected(SavedDeskParseError::kInvalidDeskType);
  }
  const ash::DeskTemplateType desk_type =
      GetDeskTypeFromString(desk_type_string);

  // If policy template set auto launch bool.
  bool auto_launch_on_startup = false;
  GetBool(value_dict, kAutoLaunchOnStartup, &auto_launch_on_startup);

  const base::Time created_time = ProtoTimeToTime(created_time_usec);
  const base::Time updated_time = ProtoTimeToTime(updated_time_usec);

  std::unique_ptr<ash::DeskTemplate> desk_template = nullptr;

  // Note: this method is responsible for parsing both regular and policy
  // templates after said policy templates are pushed to the device.
  if (auto* policy_value = value_dict.FindDict(kPolicy)) {
    desk_template = std::make_unique<ash::DeskTemplate>(
        std::move(uuid), source, name, created_time, desk_type,
        auto_launch_on_startup, base::Value(policy_value->Clone()));
  } else {
    desk_template = std::make_unique<ash::DeskTemplate>(
        std::move(uuid), source, name, created_time, desk_type);
  }

  if (desk_type == ash::DeskTemplateType::kSaveAndRecall) {
    std::string lacros_profile_id_str;
    if (GetString(value_dict, kLacrosProfileId, &lacros_profile_id_str)) {
      uint64_t lacros_profile_id = 0;
      if (base::StringToUint64(lacros_profile_id_str, &lacros_profile_id)) {
        desk_template->set_lacros_profile_id(lacros_profile_id);
      }
    }
  }

  desk_template->set_updated_time(updated_time);
  desk_template->set_desk_restore_data(ConvertJsonToRestoreData(desk));

  return base::ok(std::move(desk_template));
}

base::Value SerializeDeskTemplateAsBaseValue(
    const ash::DeskTemplate* desk,
    apps::AppRegistryCache* app_cache) {
  base::Value::Dict desk_dict;
  desk_dict.Set(kVersion, kVersionNum);
  desk_dict.Set(kUuid, desk->uuid().AsLowercaseString());
  desk_dict.Set(kName, desk->template_name());
  desk_dict.Set(kCreatedTime, base::TimeToValue(desk->created_time()));
  desk_dict.Set(kUpdatedTime, base::TimeToValue(desk->GetLastUpdatedTime()));
  desk_dict.Set(kDeskType, SerializeDeskTypeAsString(desk->type()));
  desk_dict.Set(kAutoLaunchOnStartup, desk->should_launch_on_startup());
  if (desk->type() == ash::DeskTemplateType::kSaveAndRecall &&
      desk->lacros_profile_id()) {
    desk_dict.Set(kLacrosProfileId,
                  base::NumberToString(desk->lacros_profile_id()));
  }
  desk_dict.Set(
      kDesk, ConvertRestoreDataToValue(desk->desk_restore_data(), app_cache));

  if (desk->policy_definition().type() == base::Value::Type::DICT) {
    desk_dict.Set(kPolicy, desk->policy_definition().Clone());
  }

  return base::Value(std::move(desk_dict));
}

std::unique_ptr<DeskTemplate> FromSyncProto(
    const sync_pb::WorkspaceDeskSpecifics& pb_entry) {
  base::Uuid uuid = base::Uuid::ParseCaseInsensitive(pb_entry.uuid());
  if (!uuid.is_valid())
    return nullptr;

  const base::Time created_time =
      ProtoTimeToTime(pb_entry.created_time_windows_epoch_micros());

  const ash::DeskTemplateType desk_type =
      pb_entry.has_desk_type()
          ? GetDeskTemplateTypeFromProtoType(pb_entry.desk_type())
          : ash::DeskTemplateType::kTemplate;

  if (desk_type == ash::DeskTemplateType::kUnknown) {
    return nullptr;
  }

  // Protobuf parsing enforces UTF-8 encoding for all strings.
  auto desk_template = std::make_unique<DeskTemplate>(
      std::move(uuid), ash::DeskTemplateSource::kUser, pb_entry.name(),
      created_time, desk_type);

  if (pb_entry.has_updated_time_windows_epoch_micros()) {
    desk_template->set_updated_time(
        ProtoTimeToTime(pb_entry.updated_time_windows_epoch_micros()));
  }
  desk_template->set_desk_restore_data(ConvertToRestoreData(pb_entry));
  if (pb_entry.has_client_cache_guid()) {
    desk_template->set_client_cache_guid(pb_entry.client_cache_guid());
  }
  if (pb_entry.has_device_form_factor()) {
    desk_template->set_device_form_factor(
        syncer::ToDeviceInfoFormFactor(pb_entry.device_form_factor()));
  } else {
    desk_template->set_device_form_factor(
        syncer::DeviceInfo::FormFactor::kUnknown);
  }
  return desk_template;
}

sync_pb::WorkspaceDeskSpecifics ToSyncProto(const DeskTemplate* desk_template,
                                            apps::AppRegistryCache* cache) {
  DCHECK(cache);

  sync_pb::WorkspaceDeskSpecifics pb_entry;
  FillDeskType(desk_template, &pb_entry);

  pb_entry.set_uuid(desk_template->uuid().AsLowercaseString());
  pb_entry.set_name(base::UTF16ToUTF8(desk_template->template_name()));
  pb_entry.set_created_time_windows_epoch_micros(
      TimeToProtoTime(desk_template->created_time()));
  if (desk_template->WasUpdatedSinceCreation()) {
    pb_entry.set_updated_time_windows_epoch_micros(
        TimeToProtoTime(desk_template->GetLastUpdatedTime()));
  }

  if (desk_template->desk_restore_data()) {
    FillWorkspaceDeskSpecifics(cache, desk_template->desk_restore_data(),
                               &pb_entry);
  }
  if (!desk_template->client_cache_guid().empty()) {
    pb_entry.set_client_cache_guid(desk_template->client_cache_guid());
  }
  pb_entry.set_device_form_factor(
      syncer::ToDeviceFormFactorProto(desk_template->device_form_factor()));
  return pb_entry;
}

}  // namespace desk_template_conversion

}  // namespace desks_storage