OLD | NEW |
| (Empty) |
1 // Copyright (c) 2010 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 | |
5 #include "chrome/browser/sync/glue/session_model_associator.h" | |
6 | |
7 #include <utility> | |
8 | |
9 #include "base/logging.h" | |
10 #include "base/string_util.h" | |
11 #include "base/utf_string_conversions.h" | |
12 #include "chrome/browser/profile.h" | |
13 #include "chrome/browser/sessions/session_id.h" | |
14 #include "chrome/browser/sync/profile_sync_service.h" | |
15 #include "chrome/browser/sync/syncable/syncable.h" | |
16 #include "chrome/common/notification_details.h" | |
17 #include "chrome/common/notification_service.h" | |
18 #include "chrome/common/url_constants.h" | |
19 | |
20 namespace browser_sync { | |
21 | |
22 namespace { | |
23 | |
24 static const char kNoSessionsFolderError[] = | |
25 "Server did not create the top-level sessions node. We " | |
26 "might be running against an out-of-date server."; | |
27 | |
28 } // namespace | |
29 | |
30 SessionModelAssociator::SessionModelAssociator( | |
31 ProfileSyncService* sync_service) : sync_service_(sync_service) { | |
32 DCHECK(CalledOnValidThread()); | |
33 DCHECK(sync_service_); | |
34 } | |
35 | |
36 SessionModelAssociator::~SessionModelAssociator() { | |
37 DCHECK(CalledOnValidThread()); | |
38 } | |
39 | |
40 // Sends a notification to ForeignSessionHandler to update the UI, because | |
41 // the session corresponding to the id given has changed state. | |
42 void SessionModelAssociator::Associate( | |
43 const sync_pb::SessionSpecifics* specifics, int64 sync_id) { | |
44 DCHECK(CalledOnValidThread()); | |
45 NotificationService::current()->Notify( | |
46 NotificationType::FOREIGN_SESSION_UPDATED, | |
47 NotificationService::AllSources(), | |
48 Details<int64>(&sync_id)); | |
49 } | |
50 | |
51 bool SessionModelAssociator::AssociateModels() { | |
52 DCHECK(CalledOnValidThread()); | |
53 UpdateSyncModelDataFromClient(); | |
54 return true; | |
55 } | |
56 | |
57 bool SessionModelAssociator::ChromeModelHasUserCreatedNodes( | |
58 bool* has_nodes) { | |
59 DCHECK(CalledOnValidThread()); | |
60 CHECK(has_nodes); | |
61 // This is wrong, but this function is unused, anyway. | |
62 *has_nodes = true; | |
63 return true; | |
64 } | |
65 | |
66 // Sends a notification to ForeignSessionHandler to update the UI, because | |
67 // the session corresponding to the id given has been deleted. | |
68 void SessionModelAssociator::Disassociate(int64 sync_id) { | |
69 NotificationService::current()->Notify( | |
70 NotificationType::FOREIGN_SESSION_DELETED, | |
71 NotificationService::AllSources(), | |
72 Details<int64>(&sync_id)); | |
73 } | |
74 | |
75 const sync_pb::SessionSpecifics* SessionModelAssociator:: | |
76 GetChromeNodeFromSyncId(int64 sync_id) { | |
77 sync_api::ReadTransaction trans( | |
78 sync_service_->backend()->GetUserShareHandle()); | |
79 sync_api::ReadNode node(&trans); | |
80 if (!node.InitByIdLookup(sync_id)) | |
81 return NULL; | |
82 return new sync_pb::SessionSpecifics(node.GetSessionSpecifics()); | |
83 } | |
84 | |
85 bool SessionModelAssociator::GetSyncIdForTaggedNode(const std::string* tag, | |
86 int64* sync_id) { | |
87 sync_api::ReadTransaction trans( | |
88 sync_service_->backend()->GetUserShareHandle()); | |
89 sync_api::ReadNode node(&trans); | |
90 if (!node.InitByClientTagLookup(syncable::SESSIONS, *tag)) | |
91 return false; | |
92 *sync_id = node.GetId(); | |
93 return true; | |
94 } | |
95 | |
96 int64 SessionModelAssociator::GetSyncIdFromChromeId(std::string id) { | |
97 sync_api::ReadTransaction trans( | |
98 sync_service_->backend()->GetUserShareHandle()); | |
99 sync_api::ReadNode node(&trans); | |
100 if (!node.InitByClientTagLookup(syncable::SESSIONS, id)) | |
101 return sync_api::kInvalidId; | |
102 return node.GetId(); | |
103 } | |
104 | |
105 bool SessionModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) { | |
106 DCHECK(CalledOnValidThread()); | |
107 CHECK(has_nodes); | |
108 *has_nodes = false; | |
109 sync_api::ReadTransaction trans( | |
110 sync_service_->backend()->GetUserShareHandle()); | |
111 sync_api::ReadNode root(&trans); | |
112 if (!root.InitByTagLookup(kSessionsTag)) { | |
113 LOG(ERROR) << kNoSessionsFolderError; | |
114 return false; | |
115 } | |
116 // The sync model has user created nodes iff the sessions folder has | |
117 // any children. | |
118 *has_nodes = root.GetFirstChildId() != sync_api::kInvalidId; | |
119 return true; | |
120 } | |
121 | |
122 std::string SessionModelAssociator::GetCurrentMachineTag() { | |
123 if (current_machine_tag_.empty()) | |
124 InitializeCurrentMachineTag(); | |
125 DCHECK(!current_machine_tag_.empty()); | |
126 return current_machine_tag_; | |
127 } | |
128 | |
129 void SessionModelAssociator::AppendForeignSessionFromSpecifics( | |
130 const sync_pb::SessionSpecifics* specifics, | |
131 std::vector<ForeignSession*>* session) { | |
132 ForeignSession* foreign_session = new ForeignSession(); | |
133 foreign_session->foreign_tession_tag = specifics->session_tag(); | |
134 session->insert(session->end(), foreign_session); | |
135 for (int i = 0; i < specifics->session_window_size(); i++) { | |
136 const sync_pb::SessionWindow* window = &specifics->session_window(i); | |
137 SessionWindow* session_window = new SessionWindow(); | |
138 PopulateSessionWindowFromSpecifics(session_window, window); | |
139 foreign_session->windows.insert( | |
140 foreign_session->windows.end(), session_window); | |
141 } | |
142 } | |
143 | |
144 // Fills the given vector with foreign session windows to restore. | |
145 void SessionModelAssociator::AppendForeignSessionWithID(int64 id, | |
146 std::vector<ForeignSession*>* session, sync_api::BaseTransaction* trans) { | |
147 if (id == sync_api::kInvalidId) | |
148 return; | |
149 sync_api::ReadNode node(trans); | |
150 if (!node.InitByIdLookup(id)) | |
151 return; | |
152 const sync_pb::SessionSpecifics* ref = &node.GetSessionSpecifics(); | |
153 AppendForeignSessionFromSpecifics(ref, session); | |
154 } | |
155 | |
156 void SessionModelAssociator::UpdateSyncModelDataFromClient() { | |
157 DCHECK(CalledOnValidThread()); | |
158 SessionService::SessionCallback* callback = | |
159 NewCallback(this, &SessionModelAssociator::OnGotSession); | |
160 // TODO(jerrica): Stop current race condition, possibly make new method in | |
161 // session service, which only grabs the windows from memory. | |
162 GetSessionService()->GetCurrentSession(&consumer_, callback); | |
163 } | |
164 | |
165 bool SessionModelAssociator::GetSessionDataFromSyncModel( | |
166 std::vector<ForeignSession*>* sessions) { | |
167 std::vector<const sync_pb::SessionSpecifics*> specifics; | |
168 DCHECK(CalledOnValidThread()); | |
169 sync_api::ReadTransaction trans( | |
170 sync_service_->backend()->GetUserShareHandle()); | |
171 sync_api::ReadNode root(&trans); | |
172 if (!root.InitByTagLookup(kSessionsTag)) { | |
173 LOG(ERROR) << kNoSessionsFolderError; | |
174 return false; | |
175 } | |
176 sync_api::ReadNode current_machine(&trans); | |
177 int64 current_id = (current_machine.InitByClientTagLookup(syncable::SESSIONS, | |
178 GetCurrentMachineTag())) ? current_machine.GetId() : sync_api::kInvalidId; | |
179 // Iterate through the nodes and populate the session model. | |
180 int64 id = root.GetFirstChildId(); | |
181 while (id != sync_api::kInvalidId) { | |
182 sync_api::ReadNode sync_node(&trans); | |
183 if (!sync_node.InitByIdLookup(id)) { | |
184 LOG(ERROR) << "Failed to fetch sync node for id " << id; | |
185 return false; | |
186 } | |
187 if (id != current_id) { | |
188 specifics.insert(specifics.end(), &sync_node.GetSessionSpecifics()); | |
189 } | |
190 id = sync_node.GetSuccessorId(); | |
191 } | |
192 for (std::vector<const sync_pb::SessionSpecifics*>::const_iterator i = | |
193 specifics.begin(); i != specifics.end(); ++i) { | |
194 AppendForeignSessionFromSpecifics(*i, sessions); | |
195 } | |
196 return true; | |
197 } | |
198 | |
199 SessionService* SessionModelAssociator::GetSessionService() { | |
200 DCHECK(sync_service_); | |
201 Profile* profile = sync_service_->profile(); | |
202 DCHECK(profile); | |
203 SessionService* sessions_service = profile->GetSessionService(); | |
204 DCHECK(sessions_service); | |
205 return sessions_service; | |
206 } | |
207 | |
208 void SessionModelAssociator::InitializeCurrentMachineTag() { | |
209 sync_api::WriteTransaction trans(sync_service_->backend()-> | |
210 GetUserShareHandle()); | |
211 syncable::Directory* dir = | |
212 trans.GetWrappedWriteTrans()->directory(); | |
213 current_machine_tag_ = "session_sync" + dir->cache_guid(); | |
214 } | |
215 | |
216 // See PopulateSessionSpecificsTab for use. May add functionality that includes | |
217 // the state later. | |
218 void SessionModelAssociator::PopulateSessionSpecificsNavigation( | |
219 const TabNavigation* navigation, sync_pb::TabNavigation* tab_navigation) { | |
220 tab_navigation->set_index(navigation->index()); | |
221 tab_navigation->set_virtual_url(navigation->virtual_url().spec()); | |
222 tab_navigation->set_referrer(navigation->referrer().spec()); | |
223 tab_navigation->set_title(UTF16ToUTF8(navigation->title())); | |
224 switch (navigation->transition()) { | |
225 case PageTransition::LINK: | |
226 tab_navigation->set_page_transition( | |
227 sync_pb::TabNavigation_PageTransition_LINK); | |
228 break; | |
229 case PageTransition::TYPED: | |
230 tab_navigation->set_page_transition( | |
231 sync_pb::TabNavigation_PageTransition_TYPED); | |
232 break; | |
233 case PageTransition::AUTO_BOOKMARK: | |
234 tab_navigation->set_page_transition( | |
235 sync_pb::TabNavigation_PageTransition_AUTO_BOOKMARK); | |
236 break; | |
237 case PageTransition::AUTO_SUBFRAME: | |
238 tab_navigation->set_page_transition( | |
239 sync_pb::TabNavigation_PageTransition_AUTO_SUBFRAME); | |
240 break; | |
241 case PageTransition::MANUAL_SUBFRAME: | |
242 tab_navigation->set_page_transition( | |
243 sync_pb::TabNavigation_PageTransition_MANUAL_SUBFRAME); | |
244 break; | |
245 case PageTransition::GENERATED: | |
246 tab_navigation->set_page_transition( | |
247 sync_pb::TabNavigation_PageTransition_GENERATED); | |
248 break; | |
249 case PageTransition::START_PAGE: | |
250 tab_navigation->set_page_transition( | |
251 sync_pb::TabNavigation_PageTransition_START_PAGE); | |
252 break; | |
253 case PageTransition::FORM_SUBMIT: | |
254 tab_navigation->set_page_transition( | |
255 sync_pb::TabNavigation_PageTransition_FORM_SUBMIT); | |
256 break; | |
257 case PageTransition::RELOAD: | |
258 tab_navigation->set_page_transition( | |
259 sync_pb::TabNavigation_PageTransition_RELOAD); | |
260 break; | |
261 case PageTransition::KEYWORD: | |
262 tab_navigation->set_page_transition( | |
263 sync_pb::TabNavigation_PageTransition_KEYWORD); | |
264 break; | |
265 case PageTransition::KEYWORD_GENERATED: | |
266 tab_navigation->set_page_transition( | |
267 sync_pb::TabNavigation_PageTransition_KEYWORD_GENERATED); | |
268 break; | |
269 case PageTransition::CHAIN_START: | |
270 tab_navigation->set_page_transition( | |
271 sync_pb::TabNavigation_PageTransition_CHAIN_START); | |
272 break; | |
273 case PageTransition::CHAIN_END: | |
274 tab_navigation->set_page_transition( | |
275 sync_pb::TabNavigation_PageTransition_CHAIN_END); | |
276 break; | |
277 case PageTransition::CLIENT_REDIRECT: | |
278 tab_navigation->set_navigation_qualifier( | |
279 sync_pb::TabNavigation_PageTransitionQualifier_CLIENT_REDIRECT); | |
280 break; | |
281 case PageTransition::SERVER_REDIRECT: | |
282 tab_navigation->set_navigation_qualifier( | |
283 sync_pb::TabNavigation_PageTransitionQualifier_SERVER_REDIRECT); | |
284 break; | |
285 default: | |
286 tab_navigation->set_page_transition( | |
287 sync_pb::TabNavigation_PageTransition_TYPED); | |
288 } | |
289 } | |
290 | |
291 // See PopulateSessionSpecificsWindow for use. | |
292 void SessionModelAssociator::PopulateSessionSpecificsTab( | |
293 const SessionTab* tab, sync_pb::SessionTab* session_tab) { | |
294 session_tab->set_tab_visual_index(tab->tab_visual_index); | |
295 session_tab->set_current_navigation_index( | |
296 tab->current_navigation_index); | |
297 session_tab->set_pinned(tab->pinned); | |
298 session_tab->set_extension_app_id(tab->extension_app_id); | |
299 for (std::vector<TabNavigation>::const_iterator i3 = | |
300 tab->navigations.begin(); i3 != tab->navigations.end(); ++i3) { | |
301 const TabNavigation navigation = *i3; | |
302 sync_pb::TabNavigation* tab_navigation = | |
303 session_tab->add_navigation(); | |
304 PopulateSessionSpecificsNavigation(&navigation, tab_navigation); | |
305 } | |
306 } | |
307 | |
308 // Called when populating session specifics to send to the sync model, called | |
309 // when associating models, or updating the sync model. | |
310 void SessionModelAssociator::PopulateSessionSpecificsWindow( | |
311 const SessionWindow* window, sync_pb::SessionWindow* session_window) { | |
312 session_window->set_selected_tab_index(window->selected_tab_index); | |
313 if (window->type == 1) { | |
314 session_window->set_browser_type( | |
315 sync_pb::SessionWindow_BrowserType_TYPE_NORMAL); | |
316 } else { | |
317 session_window->set_browser_type( | |
318 sync_pb::SessionWindow_BrowserType_TYPE_POPUP); | |
319 } | |
320 for (std::vector<SessionTab*>::const_iterator i2 = window->tabs.begin(); | |
321 i2 != window->tabs.end(); ++i2) { | |
322 const SessionTab* tab = *i2; | |
323 if (tab->navigations.at(tab->current_navigation_index).virtual_url() == | |
324 GURL(chrome::kChromeUINewTabURL)) { | |
325 continue; | |
326 } | |
327 sync_pb::SessionTab* session_tab = session_window->add_session_tab(); | |
328 PopulateSessionSpecificsTab(tab, session_tab); | |
329 } | |
330 } | |
331 | |
332 bool SessionModelAssociator::WindowHasNoTabsToSync( | |
333 const SessionWindow* window) { | |
334 int num_populated = 0; | |
335 for (std::vector<SessionTab*>::const_iterator i = window->tabs.begin(); | |
336 i != window->tabs.end(); ++i) { | |
337 const SessionTab* tab = *i; | |
338 if (tab->navigations.at(tab->current_navigation_index).virtual_url() == | |
339 GURL(chrome::kChromeUINewTabURL)) { | |
340 continue; | |
341 } | |
342 num_populated++; | |
343 } | |
344 if (num_populated == 0) | |
345 return true; | |
346 return false; | |
347 } | |
348 | |
349 void SessionModelAssociator::OnGotSession(int handle, | |
350 std::vector<SessionWindow*>* windows) { | |
351 sync_pb::SessionSpecifics session; | |
352 // Set the tag, then iterate through the vector of windows, extracting the | |
353 // window data, along with the tabs data and tab navigation data to populate | |
354 // the session specifics. | |
355 session.set_session_tag(GetCurrentMachineTag()); | |
356 for (std::vector<SessionWindow*>::const_iterator i = windows->begin(); | |
357 i != windows->end(); ++i) { | |
358 const SessionWindow* window = *i; | |
359 if (WindowHasNoTabsToSync(window)) { | |
360 continue; | |
361 } | |
362 sync_pb::SessionWindow* session_window = session.add_session_window(); | |
363 PopulateSessionSpecificsWindow(window, session_window); | |
364 } | |
365 bool has_nodes = false; | |
366 if (!SyncModelHasUserCreatedNodes(&has_nodes)) | |
367 return; | |
368 if (session.session_window_size() == 0 && has_nodes) | |
369 return; | |
370 sync_api::WriteTransaction trans( | |
371 sync_service_->backend()->GetUserShareHandle()); | |
372 sync_api::ReadNode root(&trans); | |
373 if (!root.InitByTagLookup(kSessionsTag)) { | |
374 LOG(ERROR) << kNoSessionsFolderError; | |
375 return; | |
376 } | |
377 UpdateSyncModel(&session, &trans, &root); | |
378 } | |
379 | |
380 void SessionModelAssociator::AppendSessionTabNavigation( | |
381 std::vector<TabNavigation>* navigations, | |
382 const sync_pb::TabNavigation* navigation) { | |
383 int index = 0; | |
384 GURL virtual_url; | |
385 GURL referrer; | |
386 string16 title; | |
387 std::string state; | |
388 PageTransition::Type transition; | |
389 if (navigation->has_index()) | |
390 index = navigation->index(); | |
391 if (navigation->has_virtual_url()) { | |
392 GURL gurl(navigation->virtual_url()); | |
393 virtual_url = gurl; | |
394 } | |
395 if (navigation->has_referrer()) { | |
396 GURL gurl(navigation->referrer()); | |
397 referrer = gurl; | |
398 } | |
399 if (navigation->has_title()) | |
400 title = UTF8ToUTF16(navigation->title()); | |
401 if (navigation->has_page_transition() || | |
402 navigation->has_navigation_qualifier()) { | |
403 switch (navigation->page_transition()) { | |
404 case sync_pb::TabNavigation_PageTransition_LINK: | |
405 transition = PageTransition::LINK; | |
406 break; | |
407 case sync_pb::TabNavigation_PageTransition_TYPED: | |
408 transition = PageTransition::TYPED; | |
409 break; | |
410 case sync_pb::TabNavigation_PageTransition_AUTO_BOOKMARK: | |
411 transition = PageTransition::AUTO_BOOKMARK; | |
412 break; | |
413 case sync_pb::TabNavigation_PageTransition_AUTO_SUBFRAME: | |
414 transition = PageTransition::AUTO_SUBFRAME; | |
415 break; | |
416 case sync_pb::TabNavigation_PageTransition_MANUAL_SUBFRAME: | |
417 transition = PageTransition::MANUAL_SUBFRAME; | |
418 break; | |
419 case sync_pb::TabNavigation_PageTransition_GENERATED: | |
420 transition = PageTransition::GENERATED; | |
421 break; | |
422 case sync_pb::TabNavigation_PageTransition_START_PAGE: | |
423 transition = PageTransition::START_PAGE; | |
424 break; | |
425 case sync_pb::TabNavigation_PageTransition_FORM_SUBMIT: | |
426 transition = PageTransition::FORM_SUBMIT; | |
427 break; | |
428 case sync_pb::TabNavigation_PageTransition_RELOAD: | |
429 transition = PageTransition::RELOAD; | |
430 break; | |
431 case sync_pb::TabNavigation_PageTransition_KEYWORD: | |
432 transition = PageTransition::KEYWORD; | |
433 break; | |
434 case sync_pb::TabNavigation_PageTransition_KEYWORD_GENERATED: | |
435 transition = PageTransition::KEYWORD_GENERATED; | |
436 break; | |
437 case sync_pb::TabNavigation_PageTransition_CHAIN_START: | |
438 transition = sync_pb::TabNavigation_PageTransition_CHAIN_START; | |
439 break; | |
440 case sync_pb::TabNavigation_PageTransition_CHAIN_END: | |
441 transition = PageTransition::CHAIN_END; | |
442 break; | |
443 default: | |
444 switch (navigation->navigation_qualifier()) { | |
445 case sync_pb:: | |
446 TabNavigation_PageTransitionQualifier_CLIENT_REDIRECT: | |
447 transition = PageTransition::CLIENT_REDIRECT; | |
448 break; | |
449 case sync_pb:: | |
450 TabNavigation_PageTransitionQualifier_SERVER_REDIRECT: | |
451 transition = PageTransition::SERVER_REDIRECT; | |
452 break; | |
453 default: | |
454 transition = PageTransition::TYPED; | |
455 } | |
456 } | |
457 } | |
458 TabNavigation tab_navigation(index, virtual_url, referrer, title, state, | |
459 transition); | |
460 navigations->insert(navigations->end(), tab_navigation); | |
461 } | |
462 | |
463 void SessionModelAssociator::PopulateSessionTabFromSpecifics( | |
464 SessionTab* session_tab, | |
465 const sync_pb::SessionTab* tab, SessionID id) { | |
466 session_tab->window_id = id; | |
467 SessionID tabID; | |
468 session_tab->tab_id = tabID; | |
469 if (tab->has_tab_visual_index()) | |
470 session_tab->tab_visual_index = tab->tab_visual_index(); | |
471 if (tab->has_current_navigation_index()) { | |
472 session_tab->current_navigation_index = | |
473 tab->current_navigation_index(); | |
474 } | |
475 if (tab->has_pinned()) | |
476 session_tab->pinned = tab->pinned(); | |
477 if (tab->has_extension_app_id()) | |
478 session_tab->extension_app_id = tab->extension_app_id(); | |
479 for (int i3 = 0; i3 < tab->navigation_size(); i3++) { | |
480 const sync_pb::TabNavigation* navigation = &tab->navigation(i3); | |
481 AppendSessionTabNavigation(&session_tab->navigations, navigation); | |
482 } | |
483 } | |
484 | |
485 void SessionModelAssociator::PopulateSessionWindowFromSpecifics( | |
486 SessionWindow* session_window, const sync_pb::SessionWindow* window) { | |
487 SessionID id; | |
488 session_window->window_id = id; | |
489 if (window->has_selected_tab_index()) | |
490 session_window->selected_tab_index = window->selected_tab_index(); | |
491 if (window->has_browser_type()) { | |
492 if (window->browser_type() == | |
493 sync_pb::SessionWindow_BrowserType_TYPE_NORMAL) { | |
494 session_window->type = 1; | |
495 } else { | |
496 session_window->type = 2; | |
497 } | |
498 } | |
499 for (int i = 0; i < window->session_tab_size(); i++) { | |
500 const sync_pb::SessionTab& tab = window->session_tab(i); | |
501 SessionTab* session_tab = new SessionTab(); | |
502 PopulateSessionTabFromSpecifics(session_tab, &tab, id); | |
503 session_window->tabs.insert(session_window->tabs.end(), session_tab); | |
504 } | |
505 } | |
506 | |
507 bool SessionModelAssociator::UpdateSyncModel( | |
508 sync_pb::SessionSpecifics* session_data, | |
509 sync_api::WriteTransaction* trans, | |
510 const sync_api::ReadNode* root) { | |
511 const std::string id = session_data->session_tag(); | |
512 sync_api::WriteNode write_node(trans); | |
513 if (!write_node.InitByClientTagLookup(syncable::SESSIONS, id)) { | |
514 sync_api::WriteNode create_node(trans); | |
515 if (!create_node.InitUniqueByCreation(syncable::SESSIONS, *root, id)) { | |
516 LOG(ERROR) << "Could not create node for session " << id; | |
517 return false; | |
518 } | |
519 create_node.SetSessionSpecifics(*session_data); | |
520 return true; | |
521 } | |
522 write_node.SetSessionSpecifics(*session_data); | |
523 return true; | |
524 } | |
525 | |
526 } // namespace browser_sync | |
527 | |
OLD | NEW |