desk_sync_bridge.cc 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447
  1. // Copyright 2021 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/desks_storage/core/desk_sync_bridge.h"
  5. #include <algorithm>
  6. #include "ash/public/cpp/desk_template.h"
  7. #include "base/bind.h"
  8. #include "base/callback_helpers.h"
  9. #include "base/check_op.h"
  10. #include "base/guid.h"
  11. #include "base/json/json_writer.h"
  12. #include "base/memory/ptr_util.h"
  13. #include "base/notreached.h"
  14. #include "base/strings/string_util.h"
  15. #include "base/strings/stringprintf.h"
  16. #include "base/strings/utf_string_conversions.h"
  17. #include "base/time/time.h"
  18. #include "build/chromeos_buildflags.h"
  19. #include "chromeos/ui/base/window_state_type.h"
  20. #include "components/account_id/account_id.h"
  21. #include "components/app_constants/constants.h"
  22. #include "components/app_restore/app_launch_info.h"
  23. #include "components/app_restore/window_info.h"
  24. #include "components/desks_storage/core/desk_model_observer.h"
  25. #include "components/desks_storage/core/desk_template_conversion.h"
  26. #include "components/desks_storage/core/desk_template_util.h"
  27. #include "components/services/app_service/public/cpp/app_launch_util.h"
  28. #include "components/services/app_service/public/cpp/app_registry_cache.h"
  29. #include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
  30. #include "components/services/app_service/public/cpp/app_types.h"
  31. #include "components/sync/model/entity_change.h"
  32. #include "components/sync/model/metadata_batch.h"
  33. #include "components/sync/model/metadata_change_list.h"
  34. #include "components/sync/model/model_type_change_processor.h"
  35. #include "components/sync/model/mutable_data_batch.h"
  36. #include "components/sync/protocol/model_type_state.pb.h"
  37. #include "components/sync/protocol/workspace_desk_specifics.pb.h"
  38. #include "third_party/abseil-cpp/absl/types/optional.h"
  39. #include "ui/base/ui_base_types.h"
  40. #include "ui/base/window_open_disposition.h"
  41. #if !BUILDFLAG(IS_CHROMEOS_LACROS)
  42. #include "chromeos/crosapi/cpp/lacros_startup_state.h" // nogncheck
  43. #endif // !BUILDFLAG(IS_CHROMEOS_LACROS)
  44. namespace desks_storage {
  45. using BrowserAppTab =
  46. sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_BrowserAppTab;
  47. using BrowserAppWindow = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow;
  48. using ArcApp = sync_pb::WorkspaceDeskSpecifics_ArcApp;
  49. using ArcAppWindowSize = sync_pb::WorkspaceDeskSpecifics_ArcApp_WindowSize;
  50. using ash::DeskTemplate;
  51. using ash::DeskTemplateSource;
  52. using ash::DeskTemplateType;
  53. using SyncDeskType = sync_pb::WorkspaceDeskSpecifics_DeskType;
  54. using WindowState = sync_pb::WorkspaceDeskSpecifics_WindowState;
  55. using WindowBound = sync_pb::WorkspaceDeskSpecifics_WindowBound;
  56. using LaunchContainer = sync_pb::WorkspaceDeskSpecifics_LaunchContainer;
  57. // Use name prefixed with Sync here to avoid name collision with original class
  58. // which isn't defined in a namespace.
  59. using SyncWindowOpenDisposition =
  60. sync_pb::WorkspaceDeskSpecifics_WindowOpenDisposition;
  61. using ProgressiveWebApp = sync_pb::WorkspaceDeskSpecifics_ProgressiveWebApp;
  62. using ChromeApp = sync_pb::WorkspaceDeskSpecifics_ChromeApp;
  63. using WorkspaceDeskSpecifics_App = sync_pb::WorkspaceDeskSpecifics_App;
  64. using SyncTabGroup = sync_pb::WorkspaceDeskSpecifics_BrowserAppWindow_TabGroup;
  65. using SyncTabGroupColor = sync_pb::WorkspaceDeskSpecifics_TabGroupColor;
  66. using TabGroupColor = tab_groups::TabGroupColorId;
  67. namespace {
  68. using syncer::ModelTypeStore;
  69. // The maximum number of templates the local storage can hold.
  70. constexpr std::size_t kMaxTemplateCount = 6u;
  71. // The maximum number of bytes a template can be.
  72. // Sync server silently ignores large items. The client-side
  73. // needs to check item size to avoid sending large items.
  74. // This limit follows precedent set by the chrome extension API:
  75. // chrome.storage.sync.QUOTA_BYTES_PER_ITEM.
  76. constexpr std::size_t kMaxTemplateSize = 8192u;
  77. // Allocate a EntityData and copies `specifics` into it.
  78. std::unique_ptr<syncer::EntityData> CopyToEntityData(
  79. const sync_pb::WorkspaceDeskSpecifics& specifics) {
  80. auto entity_data = std::make_unique<syncer::EntityData>();
  81. *entity_data->specifics.mutable_workspace_desk() = specifics;
  82. entity_data->name = specifics.uuid();
  83. entity_data->creation_time = desk_template_conversion::ProtoTimeToTime(
  84. specifics.created_time_windows_epoch_micros());
  85. return entity_data;
  86. }
  87. // Parses the content of `record_list` into `*desk_templates`.
  88. absl::optional<syncer::ModelError> ParseDeskTemplatesOnBackendSequence(
  89. std::map<base::GUID, std::unique_ptr<DeskTemplate>>* desk_templates,
  90. std::unique_ptr<ModelTypeStore::RecordList> record_list) {
  91. DCHECK(desk_templates);
  92. DCHECK(desk_templates->empty());
  93. DCHECK(record_list);
  94. for (const syncer::ModelTypeStore::Record& r : *record_list) {
  95. auto specifics = std::make_unique<sync_pb::WorkspaceDeskSpecifics>();
  96. if (specifics->ParseFromString(r.value)) {
  97. const base::GUID uuid =
  98. base::GUID::ParseCaseInsensitive(specifics->uuid());
  99. if (!uuid.is_valid()) {
  100. return syncer::ModelError(
  101. FROM_HERE,
  102. base::StringPrintf("Failed to parse WorkspaceDeskSpecifics uuid %s",
  103. specifics->uuid().c_str()));
  104. }
  105. std::unique_ptr<ash::DeskTemplate> entry =
  106. DeskSyncBridge::FromSyncProto(*specifics);
  107. if (!entry)
  108. continue;
  109. (*desk_templates)[uuid] = std::move(entry);
  110. } else {
  111. return syncer::ModelError(
  112. FROM_HERE, "Failed to deserialize WorkspaceDeskSpecifics.");
  113. }
  114. }
  115. return absl::nullopt;
  116. }
  117. // Fill `out_gurls` using tabs' URL in `browser_app_window`.
  118. void FillUrlList(const BrowserAppWindow& browser_app_window,
  119. std::vector<GURL>* out_gurls) {
  120. for (auto tab : browser_app_window.tabs()) {
  121. if (tab.has_url())
  122. out_gurls->emplace_back(tab.url());
  123. }
  124. }
  125. // Since tab groups must have completely valid fields therefore this function
  126. // exists to validate that sync tab groups are entirely valid.
  127. bool ValidSyncTabGroup(const SyncTabGroup& sync_tab_group) {
  128. return sync_tab_group.has_first_index() && sync_tab_group.has_last_index() &&
  129. sync_tab_group.has_title() && sync_tab_group.has_color();
  130. }
  131. // Converts a sync tab group color to its tab_groups::TabGroupColorId
  132. // equivalent.
  133. TabGroupColor TabGroupColorIdFromSyncTabColor(
  134. const SyncTabGroupColor& sync_color) {
  135. switch (sync_color) {
  136. // Default to grey if unknown.
  137. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_UNKNOWN_COLOR:
  138. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREY:
  139. return TabGroupColor::kGrey;
  140. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_BLUE:
  141. return TabGroupColor::kBlue;
  142. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_RED:
  143. return TabGroupColor::kRed;
  144. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_YELLOW:
  145. return TabGroupColor::kYellow;
  146. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREEN:
  147. return TabGroupColor::kGreen;
  148. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PINK:
  149. return TabGroupColor::kPink;
  150. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PURPLE:
  151. return TabGroupColor::kPurple;
  152. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_CYAN:
  153. return TabGroupColor::kCyan;
  154. case SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_ORANGE:
  155. return TabGroupColor::kOrange;
  156. };
  157. }
  158. // Instantiates a TabGroup from its sync equivalent.
  159. app_restore::TabGroupInfo FillTabGroupInfoFromProto(
  160. const SyncTabGroup& sync_tab_group) {
  161. // This function should never be called with a partially instantiated
  162. // tab group.
  163. DCHECK(ValidSyncTabGroup(sync_tab_group));
  164. return app_restore::TabGroupInfo(
  165. {static_cast<uint32_t>(sync_tab_group.first_index()),
  166. static_cast<uint32_t>(sync_tab_group.last_index())},
  167. tab_groups::TabGroupVisualData(
  168. base::UTF8ToUTF16(sync_tab_group.title()),
  169. TabGroupColorIdFromSyncTabColor(sync_tab_group.color()),
  170. sync_tab_group.is_collapsed()));
  171. }
  172. // Fill `out_group_infos` using information found in the proto's
  173. // tab group structure.
  174. void FillTabGroupInfosFromProto(
  175. const BrowserAppWindow& browser_app_window,
  176. std::vector<app_restore::TabGroupInfo>* out_group_infos) {
  177. for (const auto& group : browser_app_window.tab_groups()) {
  178. if (!ValidSyncTabGroup(group)) {
  179. continue;
  180. }
  181. out_group_infos->push_back(FillTabGroupInfoFromProto(group));
  182. }
  183. }
  184. // Get App ID from App proto.
  185. std::string GetAppId(const sync_pb::WorkspaceDeskSpecifics_App& app) {
  186. switch (app.app().app_case()) {
  187. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
  188. // Return an empty string to indicate this app is unsupported.
  189. return std::string();
  190. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow: {
  191. const bool is_lacros =
  192. #if BUILDFLAG(IS_CHROMEOS_LACROS)
  193. true;
  194. #else
  195. // Note that this will launch the browser as lacros if it is enabled,
  196. // even if it was saved as a non-lacros window (and vice-versa).
  197. crosapi::lacros_startup_state::IsLacrosEnabled() &&
  198. crosapi::lacros_startup_state::IsLacrosPrimaryEnabled();
  199. #endif // BUILDFLAG(IS_CHROMEOS_LACROS)
  200. // Browser app has a known app ID.
  201. return std::string(is_lacros ? app_constants::kLacrosAppId
  202. : app_constants::kChromeAppId);
  203. }
  204. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
  205. return app.app().chrome_app().app_id();
  206. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
  207. return app.app().progress_web_app().app_id();
  208. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp:
  209. return app.app().arc_app().app_id();
  210. }
  211. }
  212. // Convert App proto to `app_restore::AppLaunchInfo`.
  213. std::unique_ptr<app_restore::AppLaunchInfo> ConvertToAppLaunchInfo(
  214. const sync_pb::WorkspaceDeskSpecifics_App& app) {
  215. const int32_t window_id = app.window_id();
  216. const std::string app_id = GetAppId(app);
  217. if (app_id.empty())
  218. return nullptr;
  219. auto app_launch_info =
  220. std::make_unique<app_restore::AppLaunchInfo>(app_id, window_id);
  221. if (app.has_display_id())
  222. app_launch_info->display_id = app.display_id();
  223. if (app.has_container()) {
  224. app_launch_info->container = static_cast<int32_t>(
  225. desk_template_conversion::ToLaunchContainer(app.container()));
  226. }
  227. if (app.has_disposition()) {
  228. app_launch_info->disposition = static_cast<int32_t>(
  229. desk_template_conversion::ToBaseWindowOpenDisposition(
  230. app.disposition()));
  231. }
  232. if (app.has_app_name())
  233. app_launch_info->app_name = app.app_name();
  234. // This is a short-term fix as `event_flag` is required to launch ArcApp.
  235. // Currently we don't support persisting user action in template
  236. // so always default to 0 which is no action.
  237. // https://source.chromium.org/chromium/chromium/src/
  238. // +/main:ui/base/window_open_disposition.cc;l=34
  239. //
  240. // TODO(crbug.com/1311801): Add support for actual event_flag values.
  241. app_launch_info->event_flag = 0;
  242. switch (app.app().app_case()) {
  243. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::APP_NOT_SET:
  244. // This should never happen. `APP_NOT_SET` corresponds to empty `app_id`.
  245. // This method will early return when `app_id` is empty.
  246. NOTREACHED();
  247. break;
  248. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kBrowserAppWindow:
  249. if (app.app().browser_app_window().has_active_tab_index()) {
  250. app_launch_info->active_tab_index =
  251. app.app().browser_app_window().active_tab_index();
  252. }
  253. app_launch_info->urls.emplace();
  254. FillUrlList(app.app().browser_app_window(),
  255. &app_launch_info->urls.value());
  256. if (app.app().browser_app_window().tab_groups_size() > 0) {
  257. app_launch_info->tab_group_infos.emplace();
  258. FillTabGroupInfosFromProto(app.app().browser_app_window(),
  259. &app_launch_info->tab_group_infos.value());
  260. }
  261. if (app.app().browser_app_window().has_show_as_app()) {
  262. app_launch_info->app_type_browser =
  263. app.app().browser_app_window().show_as_app();
  264. }
  265. if (app.app().browser_app_window().has_first_non_pinned_tab_index()) {
  266. app_launch_info->first_non_pinned_tab_index =
  267. app.app().browser_app_window().first_non_pinned_tab_index();
  268. }
  269. break;
  270. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kChromeApp:
  271. // `app_id` is enough to identify a Chrome app.
  272. break;
  273. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kProgressWebApp:
  274. // `app_id` is enough to identify a Progressive Web app.
  275. break;
  276. case sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp:
  277. // `app_id` is enough to identify an Arc app.
  278. break;
  279. }
  280. return app_launch_info;
  281. }
  282. // Convert Sync proto WindowState `state` to ui::WindowShowState used by
  283. // the app_restore::WindowInfo struct.
  284. ui::WindowShowState ToUiWindowState(WindowState state) {
  285. switch (state) {
  286. case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
  287. return ui::WindowShowState::SHOW_STATE_NORMAL;
  288. case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
  289. return ui::WindowShowState::SHOW_STATE_NORMAL;
  290. case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
  291. return ui::WindowShowState::SHOW_STATE_MINIMIZED;
  292. case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
  293. return ui::WindowShowState::SHOW_STATE_MAXIMIZED;
  294. case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
  295. return ui::WindowShowState::SHOW_STATE_FULLSCREEN;
  296. case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
  297. return ui::WindowShowState::SHOW_STATE_NORMAL;
  298. case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
  299. return ui::WindowShowState::SHOW_STATE_NORMAL;
  300. }
  301. }
  302. // Convert Sync proto WindowState `state` to chromeos::WindowStateType used
  303. // by the app_restore::WindowInfo struct.
  304. chromeos::WindowStateType ToChromeOsWindowState(WindowState state) {
  305. switch (state) {
  306. case WindowState::WorkspaceDeskSpecifics_WindowState_UNKNOWN_WINDOW_STATE:
  307. return chromeos::WindowStateType::kNormal;
  308. case WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL:
  309. return chromeos::WindowStateType::kNormal;
  310. case WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED:
  311. return chromeos::WindowStateType::kMinimized;
  312. case WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED:
  313. return chromeos::WindowStateType::kMaximized;
  314. case WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN:
  315. return chromeos::WindowStateType::kFullscreen;
  316. case WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED:
  317. return chromeos::WindowStateType::kPrimarySnapped;
  318. case WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED:
  319. return chromeos::WindowStateType::kSecondarySnapped;
  320. }
  321. }
  322. // Convert chromeos::WindowStateType to Sync proto WindowState.
  323. WindowState FromChromeOsWindowState(chromeos::WindowStateType state) {
  324. switch (state) {
  325. case chromeos::WindowStateType::kDefault:
  326. case chromeos::WindowStateType::kNormal:
  327. case chromeos::WindowStateType::kInactive:
  328. case chromeos::WindowStateType::kPinned:
  329. case chromeos::WindowStateType::kTrustedPinned:
  330. case chromeos::WindowStateType::kPip:
  331. // TODO(crbug.com/1331825): Float state support for desk template.
  332. case chromeos::WindowStateType::kFloated:
  333. return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
  334. case chromeos::WindowStateType::kMinimized:
  335. return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
  336. case chromeos::WindowStateType::kMaximized:
  337. return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
  338. case chromeos::WindowStateType::kFullscreen:
  339. return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
  340. case chromeos::WindowStateType::kPrimarySnapped:
  341. return WindowState::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED;
  342. case chromeos::WindowStateType::kSecondarySnapped:
  343. return WindowState::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED;
  344. }
  345. }
  346. // Convert ui::WindowShowState to Sync proto WindowState.
  347. WindowState FromUiWindowState(ui::WindowShowState state) {
  348. switch (state) {
  349. case ui::WindowShowState::SHOW_STATE_DEFAULT:
  350. case ui::WindowShowState::SHOW_STATE_NORMAL:
  351. case ui::WindowShowState::SHOW_STATE_INACTIVE:
  352. case ui::WindowShowState::SHOW_STATE_END:
  353. return WindowState::WorkspaceDeskSpecifics_WindowState_NORMAL;
  354. case ui::WindowShowState::SHOW_STATE_MINIMIZED:
  355. return WindowState::WorkspaceDeskSpecifics_WindowState_MINIMIZED;
  356. case ui::WindowShowState::SHOW_STATE_MAXIMIZED:
  357. return WindowState::WorkspaceDeskSpecifics_WindowState_MAXIMIZED;
  358. case ui::WindowShowState::SHOW_STATE_FULLSCREEN:
  359. return WindowState::WorkspaceDeskSpecifics_WindowState_FULLSCREEN;
  360. }
  361. }
  362. // Converts a sync tab group color to its tab_groups::TabGroupColorId
  363. // equivalent.
  364. SyncTabGroupColor SyncTabColorFromTabGroupColorId(
  365. const TabGroupColor& sync_color) {
  366. switch (sync_color) {
  367. case TabGroupColor::kGrey:
  368. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREY;
  369. case TabGroupColor::kBlue:
  370. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_BLUE;
  371. case TabGroupColor::kRed:
  372. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_RED;
  373. case TabGroupColor::kYellow:
  374. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_YELLOW;
  375. case TabGroupColor::kGreen:
  376. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_GREEN;
  377. case TabGroupColor::kPink:
  378. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PINK;
  379. case TabGroupColor::kPurple:
  380. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_PURPLE;
  381. case TabGroupColor::kCyan:
  382. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_CYAN;
  383. case TabGroupColor::kOrange:
  384. return SyncTabGroupColor::WorkspaceDeskSpecifics_TabGroupColor_ORANGE;
  385. };
  386. }
  387. void FillSyncTabGroupInfo(const app_restore::TabGroupInfo& tab_group_info,
  388. SyncTabGroup* out_sync_tab_group) {
  389. out_sync_tab_group->set_first_index(tab_group_info.tab_range.start());
  390. out_sync_tab_group->set_last_index(tab_group_info.tab_range.end());
  391. out_sync_tab_group->set_title(
  392. base::UTF16ToUTF8(tab_group_info.visual_data.title()));
  393. // Save some storage space by leaving is_collapsed to default value if the
  394. // tab group isn't collapsed.
  395. if (tab_group_info.visual_data.is_collapsed()) {
  396. out_sync_tab_group->set_is_collapsed(
  397. tab_group_info.visual_data.is_collapsed());
  398. }
  399. out_sync_tab_group->set_color(
  400. SyncTabColorFromTabGroupColorId(tab_group_info.visual_data.color()));
  401. }
  402. void FillBrowserAppTabGroupInfos(
  403. const std::vector<app_restore::TabGroupInfo>& tab_group_infos,
  404. BrowserAppWindow* out_browser_app_window) {
  405. for (const auto& tab_group : tab_group_infos) {
  406. SyncTabGroup* sync_tab_group = out_browser_app_window->add_tab_groups();
  407. FillSyncTabGroupInfo(tab_group, sync_tab_group);
  408. }
  409. }
  410. // Fill `out_browser_app_window` with the given GURLs as BrowserAppTabs.
  411. void FillBrowserAppTabs(const std::vector<GURL>& gurls,
  412. BrowserAppWindow* out_browser_app_window) {
  413. for (const auto& gurl : gurls) {
  414. const std::string& url = gurl.spec();
  415. if (url.empty()) {
  416. // Skip invalid URLs.
  417. continue;
  418. }
  419. BrowserAppTab* browser_app_tab = out_browser_app_window->add_tabs();
  420. browser_app_tab->set_url(url);
  421. }
  422. }
  423. // Fill `out_browser_app_window` with urls and tab information from
  424. // `app_restore_data`.
  425. void FillBrowserAppWindow(const app_restore::AppRestoreData* app_restore_data,
  426. BrowserAppWindow* out_browser_app_window) {
  427. if (app_restore_data->urls.has_value())
  428. FillBrowserAppTabs(app_restore_data->urls.value(), out_browser_app_window);
  429. if (app_restore_data->active_tab_index.has_value()) {
  430. out_browser_app_window->set_active_tab_index(
  431. app_restore_data->active_tab_index.value());
  432. }
  433. if (app_restore_data->app_type_browser.has_value()) {
  434. out_browser_app_window->set_show_as_app(
  435. app_restore_data->app_type_browser.value());
  436. }
  437. if (app_restore_data->tab_group_infos.has_value()) {
  438. FillBrowserAppTabGroupInfos(app_restore_data->tab_group_infos.value(),
  439. out_browser_app_window);
  440. }
  441. if (app_restore_data->first_non_pinned_tab_index.has_value()) {
  442. out_browser_app_window->set_first_non_pinned_tab_index(
  443. app_restore_data->first_non_pinned_tab_index.value());
  444. }
  445. }
  446. // Fill `out_window_bounds` with information from `bounds`.
  447. void FillWindowBound(const gfx::Rect& bounds, WindowBound* out_window_bounds) {
  448. out_window_bounds->set_left(bounds.x());
  449. out_window_bounds->set_top(bounds.y());
  450. out_window_bounds->set_width(bounds.width());
  451. out_window_bounds->set_height(bounds.height());
  452. }
  453. // Fill `out_app` with information from `window_info`.
  454. void FillAppWithWindowInfo(const app_restore::WindowInfo* window_info,
  455. WorkspaceDeskSpecifics_App* out_app) {
  456. if (window_info->activation_index.has_value())
  457. out_app->set_z_index(window_info->activation_index.value());
  458. if (window_info->current_bounds.has_value()) {
  459. FillWindowBound(window_info->current_bounds.value(),
  460. out_app->mutable_window_bound());
  461. }
  462. if (window_info->window_state_type.has_value()) {
  463. out_app->set_window_state(
  464. FromChromeOsWindowState(window_info->window_state_type.value()));
  465. }
  466. if (window_info->pre_minimized_show_state_type.has_value()) {
  467. out_app->set_pre_minimized_window_state(
  468. FromUiWindowState(window_info->pre_minimized_show_state_type.value()));
  469. }
  470. if (window_info->snap_percentage.has_value())
  471. out_app->set_snap_percentage(window_info->snap_percentage.value());
  472. if (window_info->app_title.has_value())
  473. out_app->set_title(base::UTF16ToUTF8(window_info->app_title.value()));
  474. // AppRestoreData.GetWindowInfo does not include `display_id` in the returned
  475. // WindowInfo. Therefore, we are not filling `display_id` here.
  476. }
  477. // Fill `out_app` with the `display_id` from `app_restore_data`.
  478. void FillAppWithDisplayId(const app_restore::AppRestoreData* app_restore_data,
  479. WorkspaceDeskSpecifics_App* out_app) {
  480. if (app_restore_data->display_id.has_value())
  481. out_app->set_display_id(app_restore_data->display_id.value());
  482. }
  483. // Fill `out_app` with `container` from `app_restore_data`.
  484. void FillAppWithLaunchContainer(
  485. const app_restore::AppRestoreData* app_restore_data,
  486. WorkspaceDeskSpecifics_App* out_app) {
  487. if (app_restore_data->container.has_value()) {
  488. out_app->set_container(desk_template_conversion::FromLaunchContainer(
  489. static_cast<apps::LaunchContainer>(
  490. app_restore_data->container.value())));
  491. }
  492. }
  493. // Fill `out_app` with `disposition` from `app_restore_data`.
  494. void FillAppWithWindowOpenDisposition(
  495. const app_restore::AppRestoreData* app_restore_data,
  496. WorkspaceDeskSpecifics_App* out_app) {
  497. if (app_restore_data->disposition.has_value()) {
  498. out_app->set_disposition(
  499. desk_template_conversion::FromBaseWindowOpenDisposition(
  500. static_cast<WindowOpenDisposition>(
  501. app_restore_data->disposition.value())));
  502. }
  503. }
  504. // Fills `out_app` with `app_name` and `title` from `app_restore_data`.
  505. void FillAppWithAppNameAndTitle(
  506. const app_restore::AppRestoreData* app_restore_data,
  507. WorkspaceDeskSpecifics_App* out_app) {
  508. if (app_restore_data->app_name.has_value() &&
  509. !app_restore_data->app_name.value().empty()) {
  510. out_app->set_app_name(app_restore_data->app_name.value());
  511. }
  512. if (app_restore_data->title.has_value() &&
  513. !app_restore_data->title.value().empty()) {
  514. out_app->set_title(base::UTF16ToUTF8(app_restore_data->title.value()));
  515. }
  516. }
  517. void FillArcAppSize(const gfx::Size& size, ArcAppWindowSize* out_window_size) {
  518. out_window_size->set_width(size.width());
  519. out_window_size->set_height(size.height());
  520. }
  521. void FillArcBoundsInRoot(const gfx::Rect& data_rect, WindowBound* out_rect) {
  522. out_rect->set_left(data_rect.x());
  523. out_rect->set_top(data_rect.y());
  524. out_rect->set_width(data_rect.width());
  525. out_rect->set_height(data_rect.height());
  526. }
  527. void FillArcApp(const app_restore::AppRestoreData* app_restore_data,
  528. ArcApp* out_app) {
  529. if (app_restore_data->minimum_size.has_value()) {
  530. FillArcAppSize(app_restore_data->minimum_size.value(),
  531. out_app->mutable_minimum_size());
  532. }
  533. if (app_restore_data->maximum_size.has_value()) {
  534. FillArcAppSize(app_restore_data->maximum_size.value(),
  535. out_app->mutable_maximum_size());
  536. }
  537. if (app_restore_data->bounds_in_root.has_value()) {
  538. FillArcBoundsInRoot(app_restore_data->bounds_in_root.value(),
  539. out_app->mutable_bounds_in_root());
  540. }
  541. }
  542. // Fills an app with container and open disposition. This is only done in the
  543. // specific cases of Chrome Apps and PWAs.
  544. void FillAppWithLaunchContainerAndOpenDisposition(
  545. const app_restore::AppRestoreData* app_restore_data,
  546. WorkspaceDeskSpecifics_App* out_app) {
  547. // If present, fills the proto's `container` field with the information stored
  548. // in the `app_restore_data`'s `container` field.
  549. FillAppWithLaunchContainer(app_restore_data, out_app);
  550. // If present, fills the proto's `disposition` field with the information
  551. // stored in the `app_restore_data`'s `disposition` field.
  552. FillAppWithWindowOpenDisposition(app_restore_data, out_app);
  553. }
  554. // Fill `out_app` with `app_restore_data`.
  555. // Return `false` if app type is unsupported.
  556. bool FillApp(const std::string& app_id,
  557. const apps::AppType app_type,
  558. const app_restore::AppRestoreData* app_restore_data,
  559. WorkspaceDeskSpecifics_App* out_app) {
  560. // See definition in components/services/app_service/public/cpp/app_types.h
  561. switch (app_type) {
  562. case apps::AppType::kWeb:
  563. case apps::AppType::kSystemWeb: {
  564. // System Web Apps.
  565. // kSystemWeb is returned for System Web Apps in Lacros-primary
  566. // configuration. These can be persisted and launched the same way as
  567. // Chrome Apps.
  568. ChromeApp* chrome_app_window =
  569. out_app->mutable_app()->mutable_chrome_app();
  570. chrome_app_window->set_app_id(app_id);
  571. FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
  572. break;
  573. }
  574. case apps::AppType::kChromeApp: {
  575. // Ash Chrome browser OR PWA OR Chrome App hosted in Ash Chrome.
  576. if (app_constants::kChromeAppId == app_id) {
  577. // This window is either a browser window or a PWA window.
  578. // Both cases are persisted as "browser app" since they are launched the
  579. // same way. PWA window will have field `app_name` and
  580. // `app_type_browser` fields set. FillAppWithAppNameAndTitle has
  581. // persisted `app_name` field. FillBrowserAppWindow will persist
  582. // `app_type_browser` field.
  583. BrowserAppWindow* browser_app_window =
  584. out_app->mutable_app()->mutable_browser_app_window();
  585. FillBrowserAppWindow(app_restore_data, browser_app_window);
  586. } else {
  587. // Chrome App
  588. ChromeApp* chrome_app_window =
  589. out_app->mutable_app()->mutable_chrome_app();
  590. chrome_app_window->set_app_id(app_id);
  591. FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
  592. }
  593. break;
  594. }
  595. case apps::AppType::kStandaloneBrowser: {
  596. if (app_constants::kLacrosAppId == app_id) {
  597. // Lacros Chrome browser window or PWA hosted in Lacros Chrome.
  598. BrowserAppWindow* browser_app_window =
  599. out_app->mutable_app()->mutable_browser_app_window();
  600. FillBrowserAppWindow(app_restore_data, browser_app_window);
  601. } else {
  602. // Chrome app running in Lacros should have
  603. // AppType::kStandaloneBrowserChromeApp and never reach here.
  604. NOTREACHED();
  605. // Ignore this app type.
  606. return false;
  607. }
  608. break;
  609. }
  610. case apps::AppType::kStandaloneBrowserChromeApp: {
  611. // Chrome App hosted in Lacros.
  612. ChromeApp* chrome_app_window =
  613. out_app->mutable_app()->mutable_chrome_app();
  614. chrome_app_window->set_app_id(app_id);
  615. FillAppWithLaunchContainerAndOpenDisposition(app_restore_data, out_app);
  616. break;
  617. }
  618. case apps::AppType::kArc: {
  619. ArcApp* arc_app = out_app->mutable_app()->mutable_arc_app();
  620. arc_app->set_app_id(app_id);
  621. FillArcApp(app_restore_data, arc_app);
  622. break;
  623. }
  624. case apps::AppType::kBuiltIn:
  625. case apps::AppType::kCrostini:
  626. case apps::AppType::kPluginVm:
  627. case apps::AppType::kUnknown:
  628. case apps::AppType::kMacOs:
  629. case apps::AppType::kRemote:
  630. case apps::AppType::kBorealis:
  631. case apps::AppType::kExtension:
  632. case apps::AppType::kStandaloneBrowserExtension:
  633. // Unsupported app types will be ignored.
  634. return false;
  635. }
  636. FillAppWithWindowInfo(app_restore_data->GetWindowInfo().get(), out_app);
  637. // AppRestoreData.GetWindowInfo does not include `display_id` in the returned
  638. // WindowInfo. We need to fill the `display_id` from AppRestoreData.
  639. FillAppWithDisplayId(app_restore_data, out_app);
  640. // If present, fills the proto's `app_name` and `title` fields with the
  641. // information stored in the `app_restore_data`'s `app_name` and `title`
  642. // fields.
  643. FillAppWithAppNameAndTitle(app_restore_data, out_app);
  644. return true;
  645. }
  646. void FillArcExtraInfoFromProto(const ArcApp& app,
  647. app_restore::WindowInfo* out_window_info) {
  648. out_window_info->arc_extra_info.emplace();
  649. app_restore::WindowInfo::ArcExtraInfo& arc_info =
  650. out_window_info->arc_extra_info.value();
  651. if (app.has_minimum_size()) {
  652. arc_info.minimum_size.emplace(app.minimum_size().width(),
  653. app.minimum_size().height());
  654. }
  655. if (app.has_maximum_size()) {
  656. arc_info.maximum_size.emplace(app.maximum_size().width(),
  657. app.maximum_size().height());
  658. }
  659. if (app.has_bounds_in_root()) {
  660. arc_info.bounds_in_root.emplace(
  661. app.bounds_in_root().left(), app.bounds_in_root().top(),
  662. app.bounds_in_root().width(), app.bounds_in_root().height());
  663. }
  664. }
  665. // Fill `out_window_info` with information from Sync proto `app`.
  666. void FillWindowInfoFromProto(sync_pb::WorkspaceDeskSpecifics_App& app,
  667. app_restore::WindowInfo* out_window_info) {
  668. if (app.has_window_state() &&
  669. sync_pb::WorkspaceDeskSpecifics_WindowState_IsValid(app.window_state())) {
  670. out_window_info->window_state_type.emplace(
  671. ToChromeOsWindowState(app.window_state()));
  672. }
  673. if (app.has_window_bound()) {
  674. out_window_info->current_bounds.emplace(
  675. app.window_bound().left(), app.window_bound().top(),
  676. app.window_bound().width(), app.window_bound().height());
  677. }
  678. if (app.has_z_index())
  679. out_window_info->activation_index.emplace(app.z_index());
  680. if (app.has_display_id())
  681. out_window_info->display_id.emplace(app.display_id());
  682. if (app.has_pre_minimized_window_state() &&
  683. app.window_state() ==
  684. sync_pb::WorkspaceDeskSpecifics_WindowState_MINIMIZED) {
  685. out_window_info->pre_minimized_show_state_type.emplace(
  686. ToUiWindowState(app.pre_minimized_window_state()));
  687. }
  688. if (app.has_snap_percentage() &&
  689. (app.window_state() ==
  690. sync_pb::WorkspaceDeskSpecifics_WindowState_PRIMARY_SNAPPED ||
  691. app.window_state() ==
  692. sync_pb::WorkspaceDeskSpecifics_WindowState_SECONDARY_SNAPPED)) {
  693. out_window_info->snap_percentage.emplace(app.snap_percentage());
  694. }
  695. if (app.has_title())
  696. out_window_info->app_title.emplace(base::UTF8ToUTF16(app.title()));
  697. if (app.app().app_case() ==
  698. sync_pb::WorkspaceDeskSpecifics_AppOneOf::AppCase::kArcApp) {
  699. FillArcExtraInfoFromProto(app.app().arc_app(), out_window_info);
  700. }
  701. }
  702. // Convert a desk template to `app_restore::RestoreData`.
  703. std::unique_ptr<app_restore::RestoreData> ConvertToRestoreData(
  704. const sync_pb::WorkspaceDeskSpecifics& entry_proto) {
  705. auto restore_data = std::make_unique<app_restore::RestoreData>();
  706. for (auto app_proto : entry_proto.desk().apps()) {
  707. std::unique_ptr<app_restore::AppLaunchInfo> app_launch_info =
  708. ConvertToAppLaunchInfo(app_proto);
  709. if (!app_launch_info) {
  710. // Skip unsupported app.
  711. continue;
  712. }
  713. const std::string app_id = app_launch_info->app_id;
  714. restore_data->AddAppLaunchInfo(std::move(app_launch_info));
  715. app_restore::WindowInfo app_window_info;
  716. FillWindowInfoFromProto(app_proto, &app_window_info);
  717. restore_data->ModifyWindowInfo(app_id, app_proto.window_id(),
  718. app_window_info);
  719. }
  720. return restore_data;
  721. }
  722. // Fill a desk template `out_entry_proto` with information from
  723. // `restore_data`.
  724. void FillWorkspaceDeskSpecifics(
  725. apps::AppRegistryCache* apps_cache,
  726. const app_restore::RestoreData* restore_data,
  727. sync_pb::WorkspaceDeskSpecifics* out_entry_proto) {
  728. DCHECK(apps_cache);
  729. for (auto const& app_id_to_launch_list :
  730. restore_data->app_id_to_launch_list()) {
  731. const std::string app_id = app_id_to_launch_list.first;
  732. for (auto const& window_id_to_launch_info : app_id_to_launch_list.second) {
  733. const int window_id = window_id_to_launch_info.first;
  734. const app_restore::AppRestoreData* app_restore_data =
  735. window_id_to_launch_info.second.get();
  736. const auto app_type = apps_cache->GetAppType(app_id);
  737. WorkspaceDeskSpecifics_App* app =
  738. out_entry_proto->mutable_desk()->add_apps();
  739. app->set_window_id(window_id);
  740. if (!FillApp(app_id, app_type, app_restore_data, app)) {
  741. // Unsupported app type, remove this app entry.
  742. out_entry_proto->mutable_desk()->mutable_apps()->RemoveLast();
  743. }
  744. }
  745. }
  746. }
  747. // Fill a desk template `out_entry_proto` with the type of desk based on the
  748. // desk's type field.
  749. void FillDeskType(const DeskTemplate* desk_template,
  750. sync_pb::WorkspaceDeskSpecifics* out_entry_proto) {
  751. switch (desk_template->type()) {
  752. case DeskTemplateType::kTemplate:
  753. out_entry_proto->set_desk_type(
  754. SyncDeskType::WorkspaceDeskSpecifics_DeskType_TEMPLATE);
  755. return;
  756. case DeskTemplateType::kSaveAndRecall:
  757. out_entry_proto->set_desk_type(
  758. SyncDeskType::WorkspaceDeskSpecifics_DeskType_SAVE_AND_RECALL);
  759. return;
  760. case DeskTemplateType::kUnknown:
  761. NOTREACHED();
  762. return;
  763. }
  764. }
  765. // Takes in the Proto enum for a desk type `proto_type` and returns it's
  766. // DeskTemplateType equivalent.
  767. DeskTemplateType GetDeskTemplateTypeFromProtoType(
  768. const SyncDeskType& proto_type) {
  769. switch (proto_type) {
  770. // Treat unknown desk types as templates.
  771. case SyncDeskType::WorkspaceDeskSpecifics_DeskType_UNKNOWN_TYPE:
  772. return DeskTemplateType::kUnknown;
  773. case SyncDeskType::WorkspaceDeskSpecifics_DeskType_TEMPLATE:
  774. return DeskTemplateType::kTemplate;
  775. case SyncDeskType::WorkspaceDeskSpecifics_DeskType_SAVE_AND_RECALL:
  776. return DeskTemplateType::kSaveAndRecall;
  777. }
  778. }
  779. } // namespace
  780. DeskSyncBridge::DeskSyncBridge(
  781. std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor,
  782. syncer::OnceModelTypeStoreFactory create_store_callback,
  783. const AccountId& account_id)
  784. : ModelTypeSyncBridge(std::move(change_processor)),
  785. is_ready_(false),
  786. account_id_(account_id) {
  787. std::move(create_store_callback)
  788. .Run(syncer::WORKSPACE_DESK,
  789. base::BindOnce(&DeskSyncBridge::OnStoreCreated,
  790. weak_ptr_factory_.GetWeakPtr()));
  791. }
  792. DeskSyncBridge::~DeskSyncBridge() = default;
  793. std::unique_ptr<DeskTemplate> DeskSyncBridge::FromSyncProto(
  794. const sync_pb::WorkspaceDeskSpecifics& pb_entry) {
  795. base::GUID uuid = base::GUID::ParseCaseInsensitive(pb_entry.uuid());
  796. if (!uuid.is_valid())
  797. return nullptr;
  798. const base::Time created_time = desk_template_conversion::ProtoTimeToTime(
  799. pb_entry.created_time_windows_epoch_micros());
  800. const ash::DeskTemplateType desk_type =
  801. pb_entry.has_desk_type()
  802. ? GetDeskTemplateTypeFromProtoType(pb_entry.desk_type())
  803. : ash::DeskTemplateType::kTemplate;
  804. if (desk_type == ash::DeskTemplateType::kUnknown) {
  805. return nullptr;
  806. }
  807. // Protobuf parsing enforces UTF-8 encoding for all strings.
  808. auto desk_template = std::make_unique<DeskTemplate>(
  809. std::move(uuid), ash::DeskTemplateSource::kUser, pb_entry.name(),
  810. created_time, desk_type);
  811. if (pb_entry.has_updated_time_windows_epoch_micros()) {
  812. desk_template->set_updated_time(desk_template_conversion::ProtoTimeToTime(
  813. pb_entry.updated_time_windows_epoch_micros()));
  814. }
  815. desk_template->set_desk_restore_data(ConvertToRestoreData(pb_entry));
  816. return desk_template;
  817. }
  818. std::unique_ptr<syncer::MetadataChangeList>
  819. DeskSyncBridge::CreateMetadataChangeList() {
  820. return ModelTypeStore::WriteBatch::CreateMetadataChangeList();
  821. }
  822. absl::optional<syncer::ModelError> DeskSyncBridge::MergeSyncData(
  823. std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
  824. syncer::EntityChangeList entity_data) {
  825. // MergeSyncData will be called when Desk Template model type is enabled to
  826. // start syncing. There could be local desk templates that user has created
  827. // before enabling sync or during the time when Desk Template sync is
  828. // disabled. We should merge local and server data. We will send all
  829. // local-only templates to server and save server templates to local.
  830. UploadLocalOnlyData(metadata_change_list.get(), entity_data);
  831. // Apply server changes locally. Currently, if a template exists on both
  832. // local and server side, the server version will win.
  833. // TODO(yzd) We will add a template update timestamp and update this logic to
  834. // be: for templates that exist on both local and server side, we will keep
  835. // the one with later update timestamp.
  836. return ApplySyncChanges(std::move(metadata_change_list),
  837. std::move(entity_data));
  838. }
  839. absl::optional<syncer::ModelError> DeskSyncBridge::ApplySyncChanges(
  840. std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
  841. syncer::EntityChangeList entity_changes) {
  842. std::vector<const DeskTemplate*> added_or_updated;
  843. std::vector<std::string> removed;
  844. std::unique_ptr<ModelTypeStore::WriteBatch> batch =
  845. store_->CreateWriteBatch();
  846. for (const std::unique_ptr<syncer::EntityChange>& change : entity_changes) {
  847. const base::GUID uuid =
  848. base::GUID::ParseCaseInsensitive(change->storage_key());
  849. if (!uuid.is_valid()) {
  850. // Skip invalid storage keys.
  851. continue;
  852. }
  853. switch (change->type()) {
  854. case syncer::EntityChange::ACTION_DELETE: {
  855. if (desk_template_entries_.find(uuid) != desk_template_entries_.end()) {
  856. desk_template_entries_.erase(uuid);
  857. batch->DeleteData(uuid.AsLowercaseString());
  858. removed.push_back(uuid.AsLowercaseString());
  859. }
  860. break;
  861. }
  862. case syncer::EntityChange::ACTION_UPDATE:
  863. case syncer::EntityChange::ACTION_ADD: {
  864. const sync_pb::WorkspaceDeskSpecifics& specifics =
  865. change->data().specifics.workspace_desk();
  866. std::unique_ptr<DeskTemplate> remote_entry =
  867. DeskSyncBridge::FromSyncProto(specifics);
  868. if (!remote_entry) {
  869. // Skip invalid entries.
  870. continue;
  871. }
  872. DCHECK_EQ(uuid, remote_entry->uuid());
  873. std::string serialized_remote_entry = specifics.SerializeAsString();
  874. // Add/update the remote_entry to the model.
  875. desk_template_entries_[uuid] = std::move(remote_entry);
  876. added_or_updated.push_back(GetUserEntryByUUID(uuid));
  877. // Write to the store.
  878. batch->WriteData(uuid.AsLowercaseString(), serialized_remote_entry);
  879. break;
  880. }
  881. }
  882. }
  883. batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
  884. Commit(std::move(batch));
  885. NotifyRemoteDeskTemplateAddedOrUpdated(added_or_updated);
  886. NotifyRemoteDeskTemplateDeleted(removed);
  887. return absl::nullopt;
  888. }
  889. void DeskSyncBridge::GetData(StorageKeyList storage_keys,
  890. DataCallback callback) {
  891. auto batch = std::make_unique<syncer::MutableDataBatch>();
  892. for (const std::string& uuid : storage_keys) {
  893. const DeskTemplate* entry =
  894. GetUserEntryByUUID(base::GUID::ParseCaseInsensitive(uuid));
  895. if (!entry) {
  896. continue;
  897. }
  898. batch->Put(uuid, CopyToEntityData(ToSyncProto(entry)));
  899. }
  900. std::move(callback).Run(std::move(batch));
  901. }
  902. void DeskSyncBridge::GetAllDataForDebugging(DataCallback callback) {
  903. auto batch = std::make_unique<syncer::MutableDataBatch>();
  904. for (const auto& it : desk_template_entries_) {
  905. batch->Put(it.first.AsLowercaseString(),
  906. CopyToEntityData(ToSyncProto(it.second.get())));
  907. }
  908. std::move(callback).Run(std::move(batch));
  909. }
  910. std::string DeskSyncBridge::GetClientTag(
  911. const syncer::EntityData& entity_data) {
  912. return GetStorageKey(entity_data);
  913. }
  914. std::string DeskSyncBridge::GetStorageKey(
  915. const syncer::EntityData& entity_data) {
  916. return entity_data.specifics.workspace_desk().uuid();
  917. }
  918. DeskModel::GetAllEntriesResult DeskSyncBridge::GetAllEntries() {
  919. std::vector<const DeskTemplate*> entries;
  920. GetAllEntriesStatus status = GetAllEntries(entries);
  921. return GetAllEntriesResult(status, std::move(entries));
  922. }
  923. DeskModel::GetAllEntriesStatus DeskSyncBridge::GetAllEntries(
  924. std::vector<const DeskTemplate*>& entries) {
  925. if (!IsReady()) {
  926. return GetAllEntriesStatus::kFailure;
  927. }
  928. for (const auto& it : policy_entries_)
  929. entries.push_back(it.get());
  930. for (const auto& it : desk_template_entries_) {
  931. DCHECK_EQ(it.first, it.second->uuid());
  932. entries.push_back(it.second.get());
  933. }
  934. return GetAllEntriesStatus::kOk;
  935. }
  936. void DeskSyncBridge::GetEntryByUUID(const std::string& uuid_str,
  937. GetEntryByUuidCallback callback) {
  938. if (!IsReady()) {
  939. std::move(callback).Run(GetEntryByUuidStatus::kFailure, nullptr);
  940. return;
  941. }
  942. const base::GUID uuid = base::GUID::ParseCaseInsensitive(uuid_str);
  943. if (!uuid.is_valid()) {
  944. std::move(callback).Run(GetEntryByUuidStatus::kInvalidUuid, nullptr);
  945. return;
  946. }
  947. auto it = desk_template_entries_.find(uuid);
  948. if (it == desk_template_entries_.end()) {
  949. std::unique_ptr<DeskTemplate> policy_entry =
  950. GetAdminDeskTemplateByUUID(uuid_str);
  951. if (policy_entry) {
  952. std::move(callback).Run(GetEntryByUuidStatus::kOk,
  953. std::move(policy_entry));
  954. } else {
  955. std::move(callback).Run(GetEntryByUuidStatus::kNotFound, nullptr);
  956. }
  957. } else {
  958. std::move(callback).Run(GetEntryByUuidStatus::kOk,
  959. it->second.get()->Clone());
  960. }
  961. }
  962. void DeskSyncBridge::AddOrUpdateEntry(std::unique_ptr<DeskTemplate> new_entry,
  963. AddOrUpdateEntryCallback callback) {
  964. if (!IsReady()) {
  965. // This sync bridge has not finished initializing. Do not save the new entry
  966. // yet.
  967. std::move(callback).Run(AddOrUpdateEntryStatus::kFailure);
  968. return;
  969. }
  970. base::GUID uuid = new_entry->uuid();
  971. if (!uuid.is_valid()) {
  972. std::move(callback).Run(AddOrUpdateEntryStatus::kInvalidArgument);
  973. return;
  974. }
  975. // When a user creates a desk template locally, the desk template has `kUser`
  976. // as its source. Only user desk templates should be saved to Sync.
  977. DCHECK_EQ(DeskTemplateSource::kUser, new_entry->source());
  978. auto entry = new_entry->Clone();
  979. entry->set_template_name(
  980. base::CollapseWhitespace(new_entry->template_name(), true));
  981. std::unique_ptr<ModelTypeStore::WriteBatch> batch =
  982. store_->CreateWriteBatch();
  983. // Check the new entry size and ensure it is below the size limit.
  984. auto sync_proto = ToSyncProto(entry.get());
  985. if (sync_proto.ByteSizeLong() > kMaxTemplateSize) {
  986. std::move(callback).Run(AddOrUpdateEntryStatus::kEntryTooLarge);
  987. return;
  988. }
  989. // Add/update this entry to the store and model.
  990. auto entity_data = CopyToEntityData(sync_proto);
  991. change_processor()->Put(uuid.AsLowercaseString(), std::move(entity_data),
  992. batch->GetMetadataChangeList());
  993. std::unique_ptr<DeskTemplate> persisted_entry = FromSyncProto(sync_proto);
  994. desk_template_entries_[uuid] = std::move(persisted_entry);
  995. const DeskTemplate* result = GetUserEntryByUUID(uuid);
  996. batch->WriteData(uuid.AsLowercaseString(),
  997. ToSyncProto(result).SerializeAsString());
  998. Commit(std::move(batch));
  999. std::move(callback).Run(AddOrUpdateEntryStatus::kOk);
  1000. }
  1001. void DeskSyncBridge::DeleteEntry(const std::string& uuid_str,
  1002. DeleteEntryCallback callback) {
  1003. if (!IsReady()) {
  1004. // This sync bridge has not finished initializing.
  1005. // Cannot delete anything.
  1006. std::move(callback).Run(DeleteEntryStatus::kFailure);
  1007. return;
  1008. }
  1009. const base::GUID uuid = base::GUID::ParseCaseInsensitive(uuid_str);
  1010. if (GetUserEntryByUUID(uuid) == nullptr) {
  1011. // Consider the deletion successful if the entry does not exist.
  1012. std::move(callback).Run(DeleteEntryStatus::kOk);
  1013. return;
  1014. }
  1015. std::unique_ptr<ModelTypeStore::WriteBatch> batch =
  1016. store_->CreateWriteBatch();
  1017. change_processor()->Delete(uuid.AsLowercaseString(),
  1018. batch->GetMetadataChangeList());
  1019. desk_template_entries_.erase(uuid);
  1020. batch->DeleteData(uuid.AsLowercaseString());
  1021. Commit(std::move(batch));
  1022. std::move(callback).Run(DeleteEntryStatus::kOk);
  1023. }
  1024. void DeskSyncBridge::DeleteAllEntries(DeleteEntryCallback callback) {
  1025. DeleteEntryStatus status = DeleteAllEntries();
  1026. std::move(callback).Run(status);
  1027. }
  1028. DeskModel::DeleteEntryStatus DeskSyncBridge::DeleteAllEntries() {
  1029. if (!IsReady()) {
  1030. // This sync bridge has not finished initializing.
  1031. // Cannot delete anything.
  1032. return DeleteEntryStatus::kFailure;
  1033. }
  1034. std::unique_ptr<ModelTypeStore::WriteBatch> batch =
  1035. store_->CreateWriteBatch();
  1036. std::vector<base::GUID> all_uuids = GetAllEntryUuids();
  1037. for (const auto& uuid : all_uuids) {
  1038. change_processor()->Delete(uuid.AsLowercaseString(),
  1039. batch->GetMetadataChangeList());
  1040. batch->DeleteData(uuid.AsLowercaseString());
  1041. }
  1042. desk_template_entries_.clear();
  1043. return DeleteEntryStatus::kOk;
  1044. }
  1045. size_t DeskSyncBridge::GetEntryCount() const {
  1046. return GetSaveAndRecallDeskEntryCount() + GetDeskTemplateEntryCount();
  1047. }
  1048. size_t DeskSyncBridge::GetMaxEntryCount() const {
  1049. return GetMaxSaveAndRecallDeskEntryCount() + GetMaxDeskTemplateEntryCount();
  1050. }
  1051. // Return 0 for now since chrome sync does not support save and recall desks.
  1052. size_t DeskSyncBridge::GetSaveAndRecallDeskEntryCount() const {
  1053. return 0u;
  1054. }
  1055. size_t DeskSyncBridge::GetDeskTemplateEntryCount() const {
  1056. return desk_template_entries_.size() + policy_entries_.size();
  1057. }
  1058. // Chrome sync does not support save and recall desks yet. Return 0 for max
  1059. // count.
  1060. size_t DeskSyncBridge::GetMaxSaveAndRecallDeskEntryCount() const {
  1061. return 0u;
  1062. }
  1063. size_t DeskSyncBridge::GetMaxDeskTemplateEntryCount() const {
  1064. return kMaxTemplateCount + policy_entries_.size();
  1065. }
  1066. std::vector<base::GUID> DeskSyncBridge::GetAllEntryUuids() const {
  1067. std::vector<base::GUID> keys;
  1068. for (const auto& it : policy_entries_)
  1069. keys.push_back(it.get()->uuid());
  1070. for (const auto& it : desk_template_entries_) {
  1071. DCHECK_EQ(it.first, it.second->uuid());
  1072. keys.emplace_back(it.first);
  1073. }
  1074. return keys;
  1075. }
  1076. bool DeskSyncBridge::IsReady() const {
  1077. if (is_ready_) {
  1078. DCHECK(store_);
  1079. }
  1080. return is_ready_;
  1081. }
  1082. bool DeskSyncBridge::IsSyncing() const {
  1083. return change_processor()->IsTrackingMetadata();
  1084. }
  1085. // TODO(zhumatthew): Once desk sync bridge supports save and recall desk type,
  1086. // update this method to search the correct cache for the entry.
  1087. ash::DeskTemplate* DeskSyncBridge::FindOtherEntryWithName(
  1088. const std::u16string& name,
  1089. ash::DeskTemplateType type,
  1090. const base::GUID& uuid) const {
  1091. return desk_template_util::FindOtherEntryWithName(name, uuid,
  1092. desk_template_entries_);
  1093. }
  1094. sync_pb::WorkspaceDeskSpecifics DeskSyncBridge::ToSyncProto(
  1095. const DeskTemplate* desk_template) {
  1096. apps::AppRegistryCache* cache =
  1097. apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id_);
  1098. DCHECK(cache);
  1099. sync_pb::WorkspaceDeskSpecifics pb_entry;
  1100. FillDeskType(desk_template, &pb_entry);
  1101. pb_entry.set_uuid(desk_template->uuid().AsLowercaseString());
  1102. pb_entry.set_name(base::UTF16ToUTF8(desk_template->template_name()));
  1103. pb_entry.set_created_time_windows_epoch_micros(
  1104. desk_template_conversion::TimeToProtoTime(desk_template->created_time()));
  1105. if (desk_template->WasUpdatedSinceCreation()) {
  1106. pb_entry.set_updated_time_windows_epoch_micros(
  1107. desk_template_conversion::TimeToProtoTime(
  1108. desk_template->GetLastUpdatedTime()));
  1109. }
  1110. if (desk_template->desk_restore_data()) {
  1111. FillWorkspaceDeskSpecifics(cache, desk_template->desk_restore_data(),
  1112. &pb_entry);
  1113. }
  1114. return pb_entry;
  1115. }
  1116. const DeskTemplate* DeskSyncBridge::GetUserEntryByUUID(
  1117. const base::GUID& uuid) const {
  1118. auto it = desk_template_entries_.find(uuid);
  1119. if (it == desk_template_entries_.end())
  1120. return nullptr;
  1121. return it->second.get();
  1122. }
  1123. void DeskSyncBridge::NotifyDeskModelLoaded() {
  1124. for (DeskModelObserver& observer : observers_) {
  1125. observer.DeskModelLoaded();
  1126. }
  1127. }
  1128. void DeskSyncBridge::NotifyRemoteDeskTemplateAddedOrUpdated(
  1129. const std::vector<const DeskTemplate*>& new_entries) {
  1130. if (new_entries.empty()) {
  1131. return;
  1132. }
  1133. for (DeskModelObserver& observer : observers_) {
  1134. observer.EntriesAddedOrUpdatedRemotely(new_entries);
  1135. }
  1136. }
  1137. void DeskSyncBridge::NotifyRemoteDeskTemplateDeleted(
  1138. const std::vector<std::string>& uuids) {
  1139. if (uuids.empty()) {
  1140. return;
  1141. }
  1142. for (DeskModelObserver& observer : observers_) {
  1143. observer.EntriesRemovedRemotely(uuids);
  1144. }
  1145. }
  1146. void DeskSyncBridge::OnStoreCreated(
  1147. const absl::optional<syncer::ModelError>& error,
  1148. std::unique_ptr<syncer::ModelTypeStore> store) {
  1149. if (error) {
  1150. change_processor()->ReportError(*error);
  1151. return;
  1152. }
  1153. auto stored_desk_templates = std::make_unique<DeskEntries>();
  1154. DeskEntries* stored_desk_templates_copy = stored_desk_templates.get();
  1155. store_ = std::move(store);
  1156. store_->ReadAllDataAndPreprocess(
  1157. base::BindOnce(&ParseDeskTemplatesOnBackendSequence,
  1158. base::Unretained(stored_desk_templates_copy)),
  1159. base::BindOnce(&DeskSyncBridge::OnReadAllData,
  1160. weak_ptr_factory_.GetWeakPtr(),
  1161. std::move(stored_desk_templates)));
  1162. }
  1163. void DeskSyncBridge::OnReadAllData(
  1164. std::unique_ptr<DeskEntries> stored_desk_templates,
  1165. const absl::optional<syncer::ModelError>& error) {
  1166. DCHECK(stored_desk_templates);
  1167. if (error) {
  1168. change_processor()->ReportError(*error);
  1169. return;
  1170. }
  1171. desk_template_entries_ = std::move(*stored_desk_templates);
  1172. store_->ReadAllMetadata(base::BindOnce(&DeskSyncBridge::OnReadAllMetadata,
  1173. weak_ptr_factory_.GetWeakPtr()));
  1174. }
  1175. void DeskSyncBridge::OnReadAllMetadata(
  1176. const absl::optional<syncer::ModelError>& error,
  1177. std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
  1178. if (error) {
  1179. change_processor()->ReportError(*error);
  1180. return;
  1181. }
  1182. change_processor()->ModelReadyToSync(std::move(metadata_batch));
  1183. is_ready_ = true;
  1184. NotifyDeskModelLoaded();
  1185. }
  1186. void DeskSyncBridge::OnCommit(const absl::optional<syncer::ModelError>& error) {
  1187. if (error) {
  1188. change_processor()->ReportError(*error);
  1189. }
  1190. }
  1191. void DeskSyncBridge::Commit(std::unique_ptr<ModelTypeStore::WriteBatch> batch) {
  1192. store_->CommitWriteBatch(std::move(batch),
  1193. base::BindOnce(&DeskSyncBridge::OnCommit,
  1194. weak_ptr_factory_.GetWeakPtr()));
  1195. }
  1196. void DeskSyncBridge::UploadLocalOnlyData(
  1197. syncer::MetadataChangeList* metadata_change_list,
  1198. const syncer::EntityChangeList& entity_data) {
  1199. std::set<base::GUID> local_keys_to_upload;
  1200. for (const auto& it : desk_template_entries_) {
  1201. DCHECK_EQ(DeskTemplateSource::kUser, it.second->source());
  1202. local_keys_to_upload.insert(it.first);
  1203. }
  1204. // Strip `local_keys_to_upload` of any key (UUID) that is already known to the
  1205. // server.
  1206. for (const std::unique_ptr<syncer::EntityChange>& change : entity_data) {
  1207. local_keys_to_upload.erase(
  1208. base::GUID::ParseCaseInsensitive(change->storage_key()));
  1209. }
  1210. // Upload the local-only templates.
  1211. for (const base::GUID& uuid : local_keys_to_upload) {
  1212. change_processor()->Put(
  1213. uuid.AsLowercaseString(),
  1214. CopyToEntityData(ToSyncProto(desk_template_entries_[uuid].get())),
  1215. metadata_change_list);
  1216. }
  1217. }
  1218. bool DeskSyncBridge::HasUserTemplateWithName(const std::u16string& name) {
  1219. return std::find_if(
  1220. desk_template_entries_.begin(), desk_template_entries_.end(),
  1221. [&name](std::pair<const base::GUID,
  1222. std::unique_ptr<ash::DeskTemplate>>& entry) {
  1223. return entry.second->template_name() == name;
  1224. }) != desk_template_entries_.end();
  1225. }
  1226. bool DeskSyncBridge::HasUuid(const std::string& uuid_str) const {
  1227. const base::GUID uuid = base::GUID::ParseCaseInsensitive(uuid_str);
  1228. if (!uuid.is_valid())
  1229. return false;
  1230. return std::find_if(
  1231. desk_template_entries_.begin(), desk_template_entries_.end(),
  1232. [&uuid](
  1233. const std::pair<const base::GUID,
  1234. std::unique_ptr<ash::DeskTemplate>>& entry) {
  1235. return entry.first == uuid;
  1236. }) != desk_template_entries_.end();
  1237. }
  1238. } // namespace desks_storage