Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(769)

Unified Diff: components/sync_sessions/sessions_sync_manager.cc

Issue 2791183003: [Sync] Restore previous session if no tabbed windows present (Closed)
Patch Set: Fix android compile Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: components/sync_sessions/sessions_sync_manager.cc
diff --git a/components/sync_sessions/sessions_sync_manager.cc b/components/sync_sessions/sessions_sync_manager.cc
index 665929fcd4e255dfcdfab3ccb3a15f5bdfc88130..f37c1b37f3193cbca0beb3b110e1230f4a5de030 100644
--- a/components/sync_sessions/sessions_sync_manager.cc
+++ b/components/sync_sessions/sessions_sync_manager.cc
@@ -110,6 +110,27 @@ bool ShouldSyncTabId(SessionID::id_type tab_id) {
return true;
}
+SyncedSession::DeviceType ProtoDeviceTypeToSyncedSessionDeviceType(
+ sync_pb::SyncEnums::DeviceType proto_device_type) {
+ switch (proto_device_type) {
+ case sync_pb::SyncEnums_DeviceType_TYPE_WIN:
+ return SyncedSession::TYPE_WIN;
+ case sync_pb::SyncEnums_DeviceType_TYPE_MAC:
+ return SyncedSession::TYPE_MACOSX;
+ case sync_pb::SyncEnums_DeviceType_TYPE_LINUX:
+ return SyncedSession::TYPE_LINUX;
+ case sync_pb::SyncEnums_DeviceType_TYPE_CROS:
+ return SyncedSession::TYPE_CHROMEOS;
+ case sync_pb::SyncEnums_DeviceType_TYPE_PHONE:
+ return SyncedSession::TYPE_PHONE;
+ case sync_pb::SyncEnums_DeviceType_TYPE_TABLET:
+ return SyncedSession::TYPE_TABLET;
+ case sync_pb::SyncEnums_DeviceType_TYPE_OTHER:
+ return SyncedSession::TYPE_OTHER;
+ }
+ return SyncedSession::TYPE_OTHER;
+}
+
} // namespace
// |local_device| is owned by ProfileSyncService, its lifetime exceeds
@@ -229,115 +250,152 @@ syncer::SyncMergeResult SessionsSyncManager::MergeDataAndStartSyncing(
void SessionsSyncManager::AssociateWindows(
ReloadTabsOption option,
syncer::SyncChangeList* change_output) {
- const std::string local_tag = current_machine_tag();
- sync_pb::SessionSpecifics specifics;
- specifics.set_session_tag(local_tag);
- sync_pb::SessionHeader* header_s = specifics.mutable_header();
- SyncedSession* current_session = session_tracker_.GetSession(local_tag);
- current_session->modified_time = base::Time::Now();
- header_s->set_client_name(current_session_name_);
- header_s->set_device_type(current_device_type_);
+ // Note that |current_session| is a pointer owned by |session_tracker_|.
+ // |session_tracker_| will continue to update |current_session| under
+ // the hood so care must be taken accessing it. In particular, invoking
+ // ResetSessionTracking(..) will invalidate all the tab data within
+ // the session, hence why copies of the SyncedSession must be made ahead of
+ // time.
+ SyncedSession* current_session =
+ session_tracker_.GetSession(current_machine_tag());
+ current_session->session_name = current_session_name_;
+ current_session->device_type =
+ ProtoDeviceTypeToSyncedSessionDeviceType(current_device_type_);
+ current_session->session_tag = current_machine_tag();
- session_tracker_.ResetSessionTracking(local_tag);
SyncedWindowDelegatesGetter::SyncedWindowDelegateMap windows =
synced_window_delegates_getter()->GetSyncedWindowDelegates();
- if (option == RELOAD_TABS) {
- UMA_HISTOGRAM_COUNTS("Sync.SessionWindows", windows.size());
+ // On Android, it's possible to not have any tabbed windows if this is a cold
+ // start triggered for a custom tab. In that case, the previous session must
+ // be restored, otherwise it will be lost. On the other hand, if there is at
+ // least one tabbed window open, it's safe to overwrite the previous session
+ // entirely. See crbug.com/639009 for more info.
+ bool found_tabbed_window = false;
+ for (auto& window_iter_pair : windows) {
+ if (window_iter_pair.second->IsTypeTabbed())
+ found_tabbed_window = true;
}
- if (windows.size() == 0) {
- // Assume that the window hasn't loaded. Attempting to associate now would
- // clobber any old windows, so just return.
- LOG(ERROR) << "No windows present, see crbug.com/639009";
- return;
+
+ if (found_tabbed_window) {
+ // Just reset the session tracking. No need to worry about the previous
+ // session; the current tabbed windows are now the source of truth.
+ session_tracker_.ResetSessionTracking(current_machine_tag());
+ current_session->modified_time = base::Time::Now();
+ } else {
+ DVLOG(1) << "Found no tabbed windows. Reloading "
+ << current_session->windows.size()
+ << " windows from previous session.";
+
+ // A copy of the specifics must be made because |current_session| will be
+ // updated in place and therefore can't be relied on as the source of truth.
+ sync_pb::SessionHeader header_specifics;
+ header_specifics.CopyFrom(current_session->ToSessionHeaderProto());
+ session_tracker_.ResetSessionTracking(current_machine_tag());
+ PopulateSyncedSessionFromSpecifics(current_machine_tag(), header_specifics,
+ base::Time::Now(), current_session);
+
+ // The tab entities stored in sync have outdated SessionId values. Go
+ // through and update them to the new SessionIds.
+ for (auto& win_iter : current_session->windows) {
+ for (auto& tab : win_iter.second->wrapped_window.tabs) {
+ int sync_id = TabNodePool::kInvalidTabNodeID;
+ if (!session_tracker_.GetTabNodeFromLocalTabId(tab->tab_id.id(),
+ &sync_id) ||
+ sync_id == TabNodePool::kInvalidTabNodeID) {
+ continue;
+ }
+ DVLOG(1) << "Rewriting tab node " << sync_id << " with tab id "
+ << tab->tab_id.id();
+ AppendChangeForExistingTab(sync_id, *tab, change_output);
+ }
+ }
}
- for (auto window_iter_pair : windows) {
+
+ for (auto& window_iter_pair : windows) {
const SyncedWindowDelegate* window_delegate = window_iter_pair.second;
if (option == RELOAD_TABS) {
UMA_HISTOGRAM_COUNTS("Sync.SessionTabs", window_delegate->GetTabCount());
}
- // Make sure the window has tabs and a viewable window. The viewable window
- // check is necessary because, for example, when a browser is closed the
- // destructor is not necessarily run immediately. This means its possible
- // for us to get a handle to a browser that is about to be removed. If
- // the tab count is 0 or the window is null, the browser is about to be
- // deleted, so we ignore it.
+ // Make sure the window has tabs and a viewable window. The viewable
+ // window check is necessary because, for example, when a browser is
+ // closed the destructor is not necessarily run immediately. This means
+ // its possible for us to get a handle to a browser that is about to be
+ // removed. If the tab count is 0 or the window is null, the browser is
+ // about to be deleted, so we ignore it.
if (window_delegate->ShouldSync() && window_delegate->GetTabCount() &&
window_delegate->HasWindow()) {
sync_pb::SessionWindow window_s;
SessionID::id_type window_id = window_delegate->GetSessionId();
DVLOG(1) << "Associating window " << window_id << " with "
<< window_delegate->GetTabCount() << " tabs.";
- window_s.set_window_id(window_id);
- // Note: We don't bother to set selected tab index anymore. We still
- // consume it when receiving foreign sessions, as reading it is free, but
- // it triggers too many sync cycles with too little value to make setting
- // it worthwhile.
- if (window_delegate->IsTypeTabbed()) {
- window_s.set_browser_type(
- sync_pb::SessionWindow_BrowserType_TYPE_TABBED);
- } else if (window_delegate->IsTypePopup()) {
- window_s.set_browser_type(
- sync_pb::SessionWindow_BrowserType_TYPE_POPUP);
- } else {
- // This is a custom tab within an app. These will not be restored on
- // startup if not present.
- window_s.set_browser_type(
- sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB);
- }
bool found_tabs = false;
for (int j = 0; j < window_delegate->GetTabCount(); ++j) {
SessionID::id_type tab_id = window_delegate->GetTabIdAt(j);
SyncedTabDelegate* synced_tab = window_delegate->GetTabAt(j);
- // GetTabAt can return a null tab; in that case just skip it.
- if (!synced_tab)
+ // GetTabAt can return a null tab; in that case just skip it. Similarly,
+ // if for some reason the tab id is invalid, skip it.
+ if (!synced_tab || !ShouldSyncTabId(tab_id))
continue;
- if (!ShouldSyncTabId(tab_id)) {
- LOG(ERROR) << "Not syncing invalid tab with id " << tab_id;
- continue;
- }
-
// Placeholder tabs are those without WebContents, either because they
// were never loaded into memory or they were evicted from memory
- // (typically only on Android devices). They only have a tab id, window
- // id, and a saved synced id (corresponding to the tab node id). Note
- // that only placeholders have this sync id, as it's necessary to
- // properly reassociate the tab with the entity that was backing it.
+ // (typically only on Android devices). They only have a tab id,
+ // window id, and a saved synced id (corresponding to the tab node
+ // id). Note that only placeholders have this sync id, as it's
+ // necessary to properly reassociate the tab with the entity that was
+ // backing it.
if (synced_tab->IsPlaceholderTab()) {
// For tabs without WebContents update the |tab_id| and |window_id|,
// as it could have changed after a session restore.
if (synced_tab->GetSyncId() > TabNodePool::kInvalidTabNodeID) {
AssociateRestoredPlaceholderTab(*synced_tab, tab_id, window_id,
change_output);
+ } else {
+ DVLOG(1) << "Placeholder tab " << tab_id << " has no sync id.";
}
} else if (RELOAD_TABS == option) {
AssociateTab(synced_tab, change_output);
}
// If the tab was syncable, it would have been added to the tracker
- // either by the above Associate[RestoredPlaceholder]Tab call or by the
- // OnLocalTabModified method invoking AssociateTab directly. Therefore,
- // we can key whether this window has valid tabs based on the tab's
- // presence in the tracker.
+ // either by the above Associate[RestoredPlaceholder]Tab call or by
+ // the OnLocalTabModified method invoking AssociateTab directly.
+ // Therefore, we can key whether this window has valid tabs based on
+ // the tab's presence in the tracker.
const sessions::SessionTab* tab = nullptr;
- if (session_tracker_.LookupSessionTab(local_tag, tab_id, &tab)) {
+ if (session_tracker_.LookupSessionTab(current_machine_tag(), tab_id,
+ &tab)) {
found_tabs = true;
- window_s.add_tab(tab_id);
+
+ // Update this window's representation in the synced session tracker.
+ // This is a no-op if called multiple times.
+ session_tracker_.PutWindowInSession(current_machine_tag(), window_id);
+
+ // Put the tab in the window (must happen after the window is added
+ // to the session).
+ session_tracker_.PutTabInWindow(current_machine_tag(), window_id,
+ tab_id);
}
}
if (found_tabs) {
- sync_pb::SessionWindow* header_window = header_s->add_window();
- *header_window = window_s;
-
- // Update this window's representation in the synced session tracker.
- session_tracker_.PutWindowInSession(local_tag, window_id);
- BuildSyncedSessionFromSpecifics(
- local_tag, window_s, current_session->modified_time,
- current_session->windows[window_id].get());
+ SyncedSessionWindow* synced_session_window =
+ current_session->windows[window_id].get();
+ if (window_delegate->IsTypeTabbed()) {
+ synced_session_window->window_type =
+ sync_pb::SessionWindow_BrowserType_TYPE_TABBED;
+ } else if (window_delegate->IsTypePopup()) {
+ synced_session_window->window_type =
+ sync_pb::SessionWindow_BrowserType_TYPE_POPUP;
+ } else {
+ // This is a custom tab within an app. These will not be restored on
+ // startup if not present.
+ synced_session_window->window_type =
+ sync_pb::SessionWindow_BrowserType_TYPE_CUSTOM_TAB;
+ }
}
}
}
@@ -350,7 +408,9 @@ void SessionsSyncManager::AssociateWindows(
// if the entity specifics are identical (i.e windows, client name did
// not change).
sync_pb::EntitySpecifics entity;
- entity.mutable_session()->CopyFrom(specifics);
+ entity.mutable_session()->set_session_tag(current_machine_tag());
+ entity.mutable_session()->mutable_header()->CopyFrom(
+ current_session->ToSessionHeaderProto());
syncer::SyncData data = syncer::SyncData::CreateLocalData(
current_machine_tag(), current_session_name_, entity);
change_output->push_back(
@@ -375,10 +435,17 @@ void SessionsSyncManager::AssociateTab(SyncedTabDelegate* const tab_delegate,
<< tab_delegate->GetWindowId();
int tab_node_id = TabNodePool::kInvalidTabNodeID;
- bool existing_tab_node =
- session_tracker_.GetTabNodeFromLocalTabId(tab_id, &tab_node_id);
- CHECK_NE(TabNodePool::kInvalidTabNodeID, tab_node_id) << "crbug.com/673618";
- tab_delegate->SetSyncId(tab_node_id);
+ bool existing_tab_node = true;
+ if (session_tracker_.IsLocalTabNodeAssociated(tab_delegate->GetSyncId())) {
+ tab_node_id = tab_delegate->GetSyncId();
+ session_tracker_.ReassociateLocalTab(tab_node_id, tab_id);
+ } else {
+ existing_tab_node =
+ session_tracker_.GetTabNodeFromLocalTabId(tab_id, &tab_node_id);
+ CHECK_NE(TabNodePool::kInvalidTabNodeID, tab_node_id) << "crbug.com/673618";
+ tab_delegate->SetSyncId(tab_node_id);
+ }
+
sessions::SessionTab* session_tab =
session_tracker_.GetTab(current_machine_tag(), tab_id);
@@ -633,6 +700,12 @@ bool SessionsSyncManager::GetAllForeignSessions(
bool SessionsSyncManager::InitFromSyncModel(
const syncer::SyncDataList& sync_data,
syncer::SyncChangeList* new_changes) {
+ // Map of all rewritten local ids. Because ids are reset on each restart,
+ // and id generation happens outside of Sync, all ids from a previous local
+ // session must be rewritten in order to be valid.
+ // Key: previous session id. Value: new session id.
+ std::map<SessionID::id_type, SessionID::id_type> session_id_map;
+
bool found_current_header = false;
int bad_foreign_hash_count = 0;
for (syncer::SyncDataList::const_iterator it = sync_data.begin();
@@ -669,9 +742,37 @@ bool SessionsSyncManager::InitFromSyncModel(
if (specifics.header().has_client_name())
current_session_name_ = specifics.header().client_name();
- // TODO(zea): crbug.com/639009 update the tracker with the specifics
- // from the header node as well. This will be necessary to preserve
- // the set of open tabs when a custom tab is opened.
+ // The specifics from the SyncData are immutable. Create a mutable copy
+ // to hold the rewritten ids.
+ sync_pb::SessionSpecifics rewritten_specifics(specifics);
+
+ // Go through and generate new tab and window ids as necessary, updating
+ // the specifics in place.
+ for (auto& window :
+ *rewritten_specifics.mutable_header()->mutable_window()) {
+ session_id_map[window.window_id()] = SessionID().id();
+ window.set_window_id(session_id_map[window.window_id()]);
+
+ google::protobuf::RepeatedField<int>* tab_ids = window.mutable_tab();
+ for (int i = 0; i < tab_ids->size(); i++) {
+ auto tab_iter = session_id_map.find(tab_ids->Get(i));
+ if (tab_iter == session_id_map.end()) {
+ // SessionID::SessionID() automatically increments a static
+ // variable, forcing a new id to be generated each time.
+ session_id_map[tab_ids->Get(i)] = SessionID().id();
+ }
+ *(tab_ids->Mutable(i)) = session_id_map[tab_ids->Get(i)];
+ // Note: the tab id of the SessionTab will be updated when the tab
+ // node itself is processed.
+ }
+ }
+
+ UpdateTrackerWithSpecifics(rewritten_specifics,
+ remote.GetModifiedTime());
+
+ DVLOG(1) << "Loaded local header and rewrote " << session_id_map.size()
+ << " ids.";
+
} else {
if (specifics.has_header() || !specifics.has_tab()) {
LOG(WARNING) << "Found more than one session header node with local "
@@ -686,12 +787,33 @@ bool SessionsSyncManager::InitFromSyncModel(
new_changes->push_back(tombstone);
} else {
// This is a valid old tab node, add it to the tracker and associate
- // it.
+ // it (using the new tab id).
DVLOG(1) << "Associating local tab " << specifics.tab().tab_id()
<< " with node " << specifics.tab_node_id();
- session_tracker_.ReassociateLocalTab(specifics.tab_node_id(),
- specifics.tab().tab_id());
- UpdateTrackerWithSpecifics(specifics, remote.GetModifiedTime());
+
+ // Now file the tab under the new tab id.
+ SessionID::id_type new_tab_id = kInvalidTabID;
+ auto iter = session_id_map.find(specifics.tab().tab_id());
+ if (iter != session_id_map.end()) {
+ new_tab_id = iter->second;
+ } else {
+ session_id_map[specifics.tab().tab_id()] = SessionID().id();
+ new_tab_id = session_id_map[specifics.tab().tab_id()];
+ }
+ DVLOG(1) << "Remapping tab " << specifics.tab().tab_id() << " to "
+ << new_tab_id;
+
+ // The specifics from the SyncData are immutable. Create a mutable
+ // copy to hold the rewritten ids.
+ sync_pb::SessionSpecifics rewritten_specifics(specifics);
+ rewritten_specifics.mutable_tab()->set_tab_id(new_tab_id);
+ session_tracker_.ReassociateLocalTab(
+ rewritten_specifics.tab_node_id(), new_tab_id);
+ UpdateTrackerWithSpecifics(rewritten_specifics,
+ remote.GetModifiedTime());
+
+ session_tracker_.ReassociateLocalTab(
+ rewritten_specifics.tab_node_id(), new_tab_id);
}
}
}
@@ -703,7 +825,7 @@ bool SessionsSyncManager::InitFromSyncModel(
session_tracker_.LookupAllForeignSessions(&sessions,
SyncedSessionTracker::RAW);
for (const auto* session : sessions) {
- session_tracker_.CleanupForeignSession(session->session_tag);
+ session_tracker_.CleanupSession(session->session_tag);
}
UMA_HISTOGRAM_COUNTS_100("Sync.SessionsBadForeignHashOnMergeCount",
@@ -730,27 +852,17 @@ void SessionsSyncManager::UpdateTrackerWithSpecifics(
// Load (or create) the SyncedSession object for this client.
const sync_pb::SessionHeader& header = specifics.header();
- PopulateSessionHeaderFromSpecifics(header, modification_time, session);
// Reset the tab/window tracking for this session (must do this before
// we start calling PutWindowInSession and PutTabInWindow so that all
// unused tabs/windows get cleared by the CleanupSession(...) call).
session_tracker_.ResetSessionTracking(session_tag);
- // Process all the windows and their tab information.
- int num_windows = header.window_size();
- DVLOG(1) << "Populating " << session_tag << " with " << num_windows
- << " windows.";
-
- for (int i = 0; i < num_windows; ++i) {
- const sync_pb::SessionWindow& window_s = header.window(i);
- SessionID::id_type window_id = window_s.window_id();
- session_tracker_.PutWindowInSession(session_tag, window_id);
- BuildSyncedSessionFromSpecifics(session_tag, window_s, modification_time,
- session->windows[window_id].get());
- }
+ PopulateSyncedSessionFromSpecifics(session_tag, header, modification_time,
+ session);
+
// Delete any closed windows and unused tabs as necessary.
- session_tracker_.CleanupForeignSession(session_tag);
+ session_tracker_.CleanupSession(session_tag);
} else if (specifics.has_tab()) {
const sync_pb::SessionTab& tab_s = specifics.tab();
SessionID::id_type tab_id = tab_s.tab_id();
@@ -822,46 +934,37 @@ void SessionsSyncManager::InitializeCurrentMachineTag(
}
}
-// static
-void SessionsSyncManager::PopulateSessionHeaderFromSpecifics(
+void SessionsSyncManager::PopulateSyncedSessionFromSpecifics(
+ const std::string& session_tag,
const sync_pb::SessionHeader& header_specifics,
base::Time mtime,
- SyncedSession* session_header) {
+ SyncedSession* synced_session) {
if (header_specifics.has_client_name())
- session_header->session_name = header_specifics.client_name();
+ synced_session->session_name = header_specifics.client_name();
if (header_specifics.has_device_type()) {
- switch (header_specifics.device_type()) {
- case sync_pb::SyncEnums_DeviceType_TYPE_WIN:
- session_header->device_type = SyncedSession::TYPE_WIN;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_MAC:
- session_header->device_type = SyncedSession::TYPE_MACOSX;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_LINUX:
- session_header->device_type = SyncedSession::TYPE_LINUX;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_CROS:
- session_header->device_type = SyncedSession::TYPE_CHROMEOS;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_PHONE:
- session_header->device_type = SyncedSession::TYPE_PHONE;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_TABLET:
- session_header->device_type = SyncedSession::TYPE_TABLET;
- break;
- case sync_pb::SyncEnums_DeviceType_TYPE_OTHER:
- // Intentionally fall-through
- default:
- session_header->device_type = SyncedSession::TYPE_OTHER;
- break;
- }
+ synced_session->device_type = ProtoDeviceTypeToSyncedSessionDeviceType(
+ header_specifics.device_type());
+ }
+ synced_session->modified_time =
+ std::max(mtime, synced_session->modified_time);
+
+ // Process all the windows and their tab information.
+ int num_windows = header_specifics.window_size();
+ DVLOG(1) << "Populating " << session_tag << " with " << num_windows
+ << " windows.";
+
+ for (int i = 0; i < num_windows; ++i) {
+ const sync_pb::SessionWindow& window_s = header_specifics.window(i);
+ SessionID::id_type window_id = window_s.window_id();
+ session_tracker_.PutWindowInSession(session_tag, window_id);
+ PopulateSyncedSessionWindowFromSpecifics(
+ session_tag, window_s, synced_session->modified_time,
+ synced_session->windows[window_id].get());
}
- session_header->modified_time =
- std::max(mtime, session_header->modified_time);
}
// static
-void SessionsSyncManager::BuildSyncedSessionFromSpecifics(
+void SessionsSyncManager::PopulateSyncedSessionWindowFromSpecifics(
const std::string& session_tag,
const sync_pb::SessionWindow& specifics,
base::Time mtime,
@@ -1017,14 +1120,22 @@ void SessionsSyncManager::AssociateRestoredPlaceholderTab(
session_tracker_.GetTab(current_machine_tag(), new_tab_id);
local_tab->window_id.set_id(new_window_id);
+ AppendChangeForExistingTab(tab_delegate.GetSyncId(), *local_tab,
+ change_output);
+}
+
+void SessionsSyncManager::AppendChangeForExistingTab(
+ int sync_id,
+ const sessions::SessionTab& tab,
+ syncer::SyncChangeList* change_output) {
// Rewrite the specifics based on the reassociated SessionTab to preserve
// the new tab and window ids.
sync_pb::EntitySpecifics entity;
- entity.mutable_session()->CopyFrom(SessionTabToSpecifics(
- *local_tab, current_machine_tag(), tab_delegate.GetSyncId()));
+ entity.mutable_session()->CopyFrom(
+ SessionTabToSpecifics(tab, current_machine_tag(), sync_id));
syncer::SyncData data = syncer::SyncData::CreateLocalData(
- TabNodeIdToTag(current_machine_tag(), tab_delegate.GetSyncId()),
- current_session_name_, entity);
+ TabNodeIdToTag(current_machine_tag(), sync_id), current_session_name_,
+ entity);
change_output->push_back(
syncer::SyncChange(FROM_HERE, syncer::SyncChange::ACTION_UPDATE, data));
}
« no previous file with comments | « components/sync_sessions/sessions_sync_manager.h ('k') | components/sync_sessions/sessions_sync_manager_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698