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

Side by Side Diff: components/ntp_snippets/offline_pages/offline_page_suggestions_provider.cc

Issue 2245583002: Split OfflinePageSuggestions into two categories (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@offlinedismissed
Patch Set: Marc's comments Created 4 years, 4 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 unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "components/ntp_snippets/offline_pages/offline_page_suggestions_provide r.h" 5 #include "components/ntp_snippets/offline_pages/offline_page_suggestions_provide r.h"
6 6
7 #include <algorithm>
8
7 #include "base/bind.h" 9 #include "base/bind.h"
8 #include "base/location.h" 10 #include "base/location.h"
9 #include "base/strings/string_number_conversions.h" 11 #include "base/strings/string_number_conversions.h"
10 #include "base/strings/utf_string_conversions.h" 12 #include "base/strings/utf_string_conversions.h"
11 #include "base/threading/thread_task_runner_handle.h" 13 #include "base/threading/thread_task_runner_handle.h"
12 #include "base/values.h" 14 #include "base/values.h"
13 #include "components/ntp_snippets/pref_names.h" 15 #include "components/ntp_snippets/pref_names.h"
16 #include "components/offline_pages/client_namespace_constants.h"
14 #include "components/prefs/pref_registry_simple.h" 17 #include "components/prefs/pref_registry_simple.h"
15 #include "components/prefs/pref_service.h" 18 #include "components/prefs/pref_service.h"
19 #include "grit/components_strings.h"
20 #include "ui/base/l10n/l10n_util.h"
16 #include "ui/gfx/image/image.h" 21 #include "ui/gfx/image/image.h"
17 22
18 using offline_pages::MultipleOfflinePageItemResult; 23 using offline_pages::MultipleOfflinePageItemResult;
19 using offline_pages::OfflinePageModel; 24 using offline_pages::OfflinePageModel;
20 using offline_pages::OfflinePageItem; 25 using offline_pages::OfflinePageItem;
21 26
22 namespace ntp_snippets { 27 namespace ntp_snippets {
23 28
24 namespace { 29 namespace {
25 30
26 const int kMaxSuggestionsCount = 5; 31 const int kMaxSuggestionsCount = 5;
27 32
33 struct {
34 bool operator()(const OfflinePageItem* left,
35 const OfflinePageItem* right) const {
36 return left->last_access_time > right->last_access_time;
37 }
38 } OrderByMostRecentlyVisited;
Marc Treib 2016/08/12 10:12:53 This instantiates one global instance of the (unna
Philipp Keck 2016/08/12 10:22:55 This is from example code of std::sort. Otherwise,
Bernhard Bauer 2016/08/12 10:28:31 Is that going to create a static initializer? If s
Philipp Keck 2016/08/12 11:02:25 Done.
39
28 } // namespace 40 } // namespace
29 41
30 OfflinePageSuggestionsProvider::OfflinePageSuggestionsProvider( 42 OfflinePageSuggestionsProvider::OfflinePageSuggestionsProvider(
43 bool recent_tabs_enabled,
44 bool downloads_enabled,
31 ContentSuggestionsProvider::Observer* observer, 45 ContentSuggestionsProvider::Observer* observer,
32 CategoryFactory* category_factory, 46 CategoryFactory* category_factory,
33 OfflinePageModel* offline_page_model, 47 OfflinePageModel* offline_page_model,
34 PrefService* pref_service) 48 PrefService* pref_service)
35 : ContentSuggestionsProvider(observer, category_factory), 49 : ContentSuggestionsProvider(observer, category_factory),
36 category_status_(CategoryStatus::AVAILABLE_LOADING), 50 recent_tabs_status_(recent_tabs_enabled
51 ? CategoryStatus::AVAILABLE_LOADING
52 : CategoryStatus::NOT_PROVIDED),
53 downloads_status_(downloads_enabled ? CategoryStatus::AVAILABLE_LOADING
54 : CategoryStatus::NOT_PROVIDED),
37 offline_page_model_(offline_page_model), 55 offline_page_model_(offline_page_model),
38 provided_category_( 56 recent_tabs_category_(
39 category_factory->FromKnownCategory(KnownCategories::OFFLINE_PAGES)), 57 category_factory->FromKnownCategory(KnownCategories::RECENT_TABS)),
40 pref_service_(pref_service) { 58 downloads_category_(
59 category_factory->FromKnownCategory(KnownCategories::DOWNLOADS)),
60 pref_service_(pref_service),
61 dismissed_recent_tab_ids_(ReadDismissedIDsFromPrefs(
62 prefs::kDismissedRecentOfflineTabSuggestions)),
63 dismissed_download_ids_(
64 ReadDismissedIDsFromPrefs(prefs::kDismissedDownloadSuggestions)) {
65 DCHECK(recent_tabs_enabled || downloads_enabled);
41 offline_page_model_->AddObserver(this); 66 offline_page_model_->AddObserver(this);
42 ReadDismissedIDsFromPrefs();
43 FetchOfflinePages(); 67 FetchOfflinePages();
44 } 68 }
45 69
46 OfflinePageSuggestionsProvider::~OfflinePageSuggestionsProvider() { 70 OfflinePageSuggestionsProvider::~OfflinePageSuggestionsProvider() {
47 offline_page_model_->RemoveObserver(this); 71 offline_page_model_->RemoveObserver(this);
48 } 72 }
49 73
50 // static 74 // static
51 void OfflinePageSuggestionsProvider::RegisterProfilePrefs( 75 void OfflinePageSuggestionsProvider::RegisterProfilePrefs(
52 PrefRegistrySimple* registry) { 76 PrefRegistrySimple* registry) {
53 registry->RegisterListPref(prefs::kDismissedOfflinePageSuggestions); 77 registry->RegisterListPref(prefs::kDismissedRecentOfflineTabSuggestions);
78 registry->RegisterListPref(prefs::kDismissedDownloadSuggestions);
54 } 79 }
55 80
56 //////////////////////////////////////////////////////////////////////////////// 81 ////////////////////////////////////////////////////////////////////////////////
57 // Private methods 82 // Private methods
58 83
59 std::vector<Category> OfflinePageSuggestionsProvider::GetProvidedCategories() { 84 std::vector<Category> OfflinePageSuggestionsProvider::GetProvidedCategories() {
60 return std::vector<Category>({provided_category_}); 85 std::vector<Category> provided_categories;
86 if (recent_tabs_status_ != CategoryStatus::NOT_PROVIDED)
87 provided_categories.push_back(recent_tabs_category_);
88 if (downloads_status_ != CategoryStatus::NOT_PROVIDED)
89 provided_categories.push_back(downloads_category_);
90 return provided_categories;
61 } 91 }
62 92
63 CategoryStatus OfflinePageSuggestionsProvider::GetCategoryStatus( 93 CategoryStatus OfflinePageSuggestionsProvider::GetCategoryStatus(
64 Category category) { 94 Category category) {
65 DCHECK_EQ(category, provided_category_); 95 if (category == recent_tabs_category_)
66 return category_status_; 96 return recent_tabs_status_;
97 if (category == downloads_category_)
98 return downloads_status_;
99 NOTREACHED() << "Unknown category " << category.id();
100 return CategoryStatus::NOT_PROVIDED;
67 } 101 }
68 102
69 CategoryInfo OfflinePageSuggestionsProvider::GetCategoryInfo( 103 CategoryInfo OfflinePageSuggestionsProvider::GetCategoryInfo(
70 Category category) { 104 Category category) {
71 // TODO(pke): Use the proper string once it's agreed on. 105 if (category == recent_tabs_category_) {
72 return CategoryInfo(base::ASCIIToUTF16("Offline pages"), 106 return CategoryInfo(l10n_util::GetStringUTF16(
107 IDS_NTP_RECENT_TAB_SUGGESTIONS_SECTION_HEADER),
108 ContentSuggestionsCardLayout::MINIMAL_CARD);
109 }
110 if (category == downloads_category_) {
111 return CategoryInfo(
112 l10n_util::GetStringUTF16(IDS_NTP_DOWNLOAD_SUGGESTIONS_SECTION_HEADER),
113 ContentSuggestionsCardLayout::MINIMAL_CARD);
114 }
115 NOTREACHED() << "Unknown category " << category.id();
116 return CategoryInfo(base::string16(),
73 ContentSuggestionsCardLayout::MINIMAL_CARD); 117 ContentSuggestionsCardLayout::MINIMAL_CARD);
74 } 118 }
75 119
76 void OfflinePageSuggestionsProvider::DismissSuggestion( 120 void OfflinePageSuggestionsProvider::DismissSuggestion(
77 const std::string& suggestion_id) { 121 const std::string& suggestion_id) {
122 Category category = GetCategoryFromUniqueID(suggestion_id);
78 std::string offline_page_id = GetWithinCategoryIDFromUniqueID(suggestion_id); 123 std::string offline_page_id = GetWithinCategoryIDFromUniqueID(suggestion_id);
79 dismissed_ids_.insert(offline_page_id); 124 if (category == recent_tabs_category_) {
80 StoreDismissedIDsToPrefs(); 125 DCHECK_NE(recent_tabs_status_, CategoryStatus::NOT_PROVIDED);
Bernhard Bauer 2016/08/12 10:28:31 The (not 😉) expected value should go first for nic
Philipp Keck 2016/08/12 11:02:25 Done.
126 dismissed_recent_tab_ids_.insert(offline_page_id);
127 StoreDismissedIDsToPrefs(prefs::kDismissedRecentOfflineTabSuggestions,
128 dismissed_recent_tab_ids_);
129 } else if (category == downloads_category_) {
130 DCHECK_NE(downloads_status_, CategoryStatus::NOT_PROVIDED);
131 dismissed_download_ids_.insert(offline_page_id);
132 StoreDismissedIDsToPrefs(prefs::kDismissedDownloadSuggestions,
133 dismissed_download_ids_);
134 } else {
135 NOTREACHED() << "Unknown category " << category.id();
136 }
81 } 137 }
82 138
83 void OfflinePageSuggestionsProvider::FetchSuggestionImage( 139 void OfflinePageSuggestionsProvider::FetchSuggestionImage(
84 const std::string& suggestion_id, 140 const std::string& suggestion_id,
85 const ImageFetchedCallback& callback) { 141 const ImageFetchedCallback& callback) {
86 // TODO(pke): Fetch proper thumbnail from OfflinePageModel once it's available 142 // TODO(pke): Fetch proper thumbnail from OfflinePageModel once it's available
87 // there. 143 // there.
88 base::ThreadTaskRunnerHandle::Get()->PostTask( 144 base::ThreadTaskRunnerHandle::Get()->PostTask(
89 FROM_HERE, base::Bind(callback, suggestion_id, gfx::Image())); 145 FROM_HERE, base::Bind(callback, suggestion_id, gfx::Image()));
90 } 146 }
91 147
92 void OfflinePageSuggestionsProvider::ClearCachedSuggestionsForDebugging( 148 void OfflinePageSuggestionsProvider::ClearCachedSuggestionsForDebugging(
93 Category category) { 149 Category category) {
94 DCHECK_EQ(category, provided_category_);
95 // Ignored. 150 // Ignored.
96 } 151 }
97 152
98 std::vector<ContentSuggestion> 153 std::vector<ContentSuggestion>
99 OfflinePageSuggestionsProvider::GetDismissedSuggestionsForDebugging( 154 OfflinePageSuggestionsProvider::GetDismissedSuggestionsForDebugging(
100 Category category) { 155 Category category) {
101 // TODO(pke): Make GetDismissedSuggestionsForDebugging asynchronous so this 156 // TODO(pke): Make GetDismissedSuggestionsForDebugging asynchronous so this
102 // can return proper values. 157 // can return proper values.
103 DCHECK_EQ(category, provided_category_);
104 std::vector<ContentSuggestion> suggestions; 158 std::vector<ContentSuggestion> suggestions;
105 for (const std::string& dismissed_id : dismissed_ids_) { 159 const std::set<std::string>* dismissed_ids = nullptr;
160 if (category == recent_tabs_category_) {
161 DCHECK_NE(recent_tabs_status_, CategoryStatus::NOT_PROVIDED);
162 dismissed_ids = &dismissed_recent_tab_ids_;
163 } else if (category == downloads_category_) {
164 DCHECK_NE(downloads_status_, CategoryStatus::NOT_PROVIDED);
165 dismissed_ids = &dismissed_download_ids_;
166 } else {
167 NOTREACHED() << "Unknown category " << category.id();
168 return suggestions;
169 }
170
171 for (const std::string& dismissed_id : *dismissed_ids) {
106 ContentSuggestion suggestion( 172 ContentSuggestion suggestion(
107 MakeUniqueID(provided_category_, dismissed_id), 173 MakeUniqueID(category, dismissed_id),
108 GURL("http://dismissed-offline-page-" + dismissed_id)); 174 GURL("http://dismissed-offline-page-" + dismissed_id));
109 suggestion.set_title(base::UTF8ToUTF16("Title not available")); 175 suggestion.set_title(base::UTF8ToUTF16("Title not available"));
110 suggestions.push_back(std::move(suggestion)); 176 suggestions.push_back(std::move(suggestion));
111 } 177 }
112 return suggestions; 178 return suggestions;
113 } 179 }
114 180
115 void OfflinePageSuggestionsProvider::ClearDismissedSuggestionsForDebugging( 181 void OfflinePageSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
116 Category category) { 182 Category category) {
117 DCHECK_EQ(category, provided_category_); 183 if (category == recent_tabs_category_) {
118 dismissed_ids_.clear(); 184 DCHECK_NE(recent_tabs_status_, CategoryStatus::NOT_PROVIDED);
119 StoreDismissedIDsToPrefs(); 185 dismissed_recent_tab_ids_.clear();
186 StoreDismissedIDsToPrefs(prefs::kDismissedRecentOfflineTabSuggestions,
187 dismissed_recent_tab_ids_);
188 } else if (category == downloads_category_) {
189 DCHECK_NE(downloads_status_, CategoryStatus::NOT_PROVIDED);
190 dismissed_download_ids_.clear();
191 StoreDismissedIDsToPrefs(prefs::kDismissedDownloadSuggestions,
192 dismissed_download_ids_);
193 } else {
194 NOTREACHED() << "Unknown category " << category.id();
195 }
120 FetchOfflinePages(); 196 FetchOfflinePages();
121 } 197 }
122 198
123 void OfflinePageSuggestionsProvider::OfflinePageModelLoaded( 199 void OfflinePageSuggestionsProvider::OfflinePageModelLoaded(
124 OfflinePageModel* model) { 200 OfflinePageModel* model) {
125 DCHECK_EQ(offline_page_model_, model); 201 DCHECK_EQ(offline_page_model_, model);
126 } 202 }
127 203
128 void OfflinePageSuggestionsProvider::OfflinePageModelChanged( 204 void OfflinePageSuggestionsProvider::OfflinePageModelChanged(
129 OfflinePageModel* model) { 205 OfflinePageModel* model) {
130 DCHECK_EQ(offline_page_model_, model); 206 DCHECK_EQ(offline_page_model_, model);
131 FetchOfflinePages(); 207 FetchOfflinePages();
132 } 208 }
133 209
134 void OfflinePageSuggestionsProvider::OfflinePageDeleted( 210 void OfflinePageSuggestionsProvider::OfflinePageDeleted(
135 int64_t offline_id, 211 int64_t offline_id,
136 const offline_pages::ClientId& client_id) { 212 const offline_pages::ClientId& client_id) {
137 // TODO(pke): Implement, suggestion has to be removed from UI immediately. 213 // TODO(pke): Implement, suggestion has to be removed from UI immediately.
138 } 214 }
139 215
140 void OfflinePageSuggestionsProvider::FetchOfflinePages() { 216 void OfflinePageSuggestionsProvider::FetchOfflinePages() {
141 offline_page_model_->GetAllPages( 217 offline_page_model_->GetAllPages(
142 base::Bind(&OfflinePageSuggestionsProvider::OnOfflinePagesLoaded, 218 base::Bind(&OfflinePageSuggestionsProvider::OnOfflinePagesLoaded,
143 base::Unretained(this))); 219 base::Unretained(this)));
144 } 220 }
145 221
146 void OfflinePageSuggestionsProvider::OnOfflinePagesLoaded( 222 void OfflinePageSuggestionsProvider::OnOfflinePagesLoaded(
147 const MultipleOfflinePageItemResult& result) { 223 const MultipleOfflinePageItemResult& result) {
148 NotifyStatusChanged(CategoryStatus::AVAILABLE); 224 bool need_recent_tabs = recent_tabs_status_ != CategoryStatus::NOT_PROVIDED;
225 bool need_downloads = downloads_status_ != CategoryStatus::NOT_PROVIDED;
226 if (need_recent_tabs)
227 NotifyStatusChanged(recent_tabs_category_, CategoryStatus::AVAILABLE);
228 if (need_downloads)
229 NotifyStatusChanged(downloads_category_, CategoryStatus::AVAILABLE);
149 230
150 std::vector<ContentSuggestion> suggestions; 231 std::vector<const OfflinePageItem*> recent_tab_items;
232 std::vector<const OfflinePageItem*> download_items;
151 for (const OfflinePageItem& item : result) { 233 for (const OfflinePageItem& item : result) {
152 if (dismissed_ids_.count(base::IntToString(item.offline_id))) 234 if (need_recent_tabs &&
153 continue; 235 item.client_id.name_space == offline_pages::kLastNNamespace) {
154 suggestions.push_back(ConvertOfflinePage(item)); 236 if (!dismissed_recent_tab_ids_.count(base::IntToString(item.offline_id)))
155 if (suggestions.size() == kMaxSuggestionsCount) 237 recent_tab_items.push_back(&item);
156 break; 238 } else if (need_downloads &&
239 item.client_id.name_space == offline_pages::kDownloadNamespace) {
240 if (!dismissed_download_ids_.count(base::IntToString(item.offline_id)))
241 download_items.push_back(&item);
242 }
157 } 243 }
158 244
159 observer()->OnNewSuggestions(this, provided_category_, 245 // TODO(pke): Once we have our OfflinePageModel getter and that doesn't do it
160 std::move(suggestions)); 246 // already, filter out duplicate URLs for recent tabs here. Duplicates for
247 // downloads are fine.
248
249 if (need_recent_tabs) {
250 observer()->OnNewSuggestions(
251 this, recent_tabs_category_,
252 GetMostRecentlyVisited(recent_tabs_category_,
253 std::move(recent_tab_items)));
254 }
255 if (need_downloads) {
256 observer()->OnNewSuggestions(
257 this, downloads_category_,
258 GetMostRecentlyVisited(downloads_category_, std::move(download_items)));
259 }
161 } 260 }
162 261
163 void OfflinePageSuggestionsProvider::NotifyStatusChanged( 262 void OfflinePageSuggestionsProvider::NotifyStatusChanged(
263 Category category,
164 CategoryStatus new_status) { 264 CategoryStatus new_status) {
165 if (category_status_ == new_status) 265 if (category == recent_tabs_category_) {
166 return; 266 DCHECK_NE(recent_tabs_status_, CategoryStatus::NOT_PROVIDED);
167 category_status_ = new_status; 267 if (recent_tabs_status_ == new_status)
168 268 return;
169 observer()->OnCategoryStatusChanged(this, provided_category_, new_status); 269 recent_tabs_status_ = new_status;
270 observer()->OnCategoryStatusChanged(this, category, new_status);
271 } else if (category == downloads_category_) {
272 DCHECK_NE(downloads_status_, CategoryStatus::NOT_PROVIDED);
273 if (downloads_status_ == new_status)
274 return;
275 downloads_status_ = new_status;
276 observer()->OnCategoryStatusChanged(this, category, new_status);
277 } else {
278 NOTREACHED() << "Unknown category " << category.id();
279 }
170 } 280 }
171 281
172 ContentSuggestion OfflinePageSuggestionsProvider::ConvertOfflinePage( 282 ContentSuggestion OfflinePageSuggestionsProvider::ConvertOfflinePage(
283 Category category,
173 const OfflinePageItem& offline_page) const { 284 const OfflinePageItem& offline_page) const {
174 // TODO(pke): Make sure the URL is actually opened as an offline URL. 285 // TODO(pke): Make sure the URL is actually opened as an offline URL.
175 // Currently, the browser opens the offline URL and then immediately 286 // Currently, the browser opens the offline URL and then immediately
176 // redirects to the online URL if the device is online. 287 // redirects to the online URL if the device is online.
177 ContentSuggestion suggestion( 288 ContentSuggestion suggestion(
178 MakeUniqueID(provided_category_, 289 MakeUniqueID(category, base::IntToString(offline_page.offline_id)),
179 base::IntToString(offline_page.offline_id)),
180 offline_page.GetOfflineURL()); 290 offline_page.GetOfflineURL());
181 291
182 // TODO(pke): Sort by most recently visited and only keep the top one of
183 // multiple entries for the same URL.
184 // TODO(pke): Get more reasonable data from the OfflinePageModel here. 292 // TODO(pke): Get more reasonable data from the OfflinePageModel here.
185 suggestion.set_title(base::UTF8ToUTF16(offline_page.url.spec())); 293 suggestion.set_title(base::UTF8ToUTF16(offline_page.url.spec()));
186 suggestion.set_snippet_text(base::string16()); 294 suggestion.set_snippet_text(base::string16());
187 suggestion.set_publish_date(offline_page.creation_time); 295 suggestion.set_publish_date(offline_page.creation_time);
188 suggestion.set_publisher_name(base::UTF8ToUTF16(offline_page.url.host())); 296 suggestion.set_publisher_name(base::UTF8ToUTF16(offline_page.url.host()));
189 return suggestion; 297 return suggestion;
190 } 298 }
191 299
192 void OfflinePageSuggestionsProvider::ReadDismissedIDsFromPrefs() { 300 std::vector<ContentSuggestion>
193 dismissed_ids_.clear(); 301 OfflinePageSuggestionsProvider::GetMostRecentlyVisited(
194 const base::ListValue* list = 302 Category category,
195 pref_service_->GetList(prefs::kDismissedOfflinePageSuggestions); 303 std::vector<const OfflinePageItem*> offline_page_items) const {
304 std::sort(offline_page_items.begin(), offline_page_items.end(),
305 OrderByMostRecentlyVisited);
306 std::vector<ContentSuggestion> suggestions;
307 for (const OfflinePageItem* offline_page_item : offline_page_items) {
308 suggestions.push_back(ConvertOfflinePage(category, *offline_page_item));
309 if (suggestions.size() == kMaxSuggestionsCount)
310 break;
311 }
312 return suggestions;
313 }
314
315 std::set<std::string> OfflinePageSuggestionsProvider::ReadDismissedIDsFromPrefs(
316 const std::string& pref_name) const {
317 std::set<std::string> dismissed_ids;
318 const base::ListValue* list = pref_service_->GetList(pref_name);
196 for (const std::unique_ptr<base::Value>& value : *list) { 319 for (const std::unique_ptr<base::Value>& value : *list) {
197 std::string dismissed_id; 320 std::string dismissed_id;
198 bool success = value->GetAsString(&dismissed_id); 321 bool success = value->GetAsString(&dismissed_id);
199 DCHECK(success) << "Failed to parse dismissed offline page ID from prefs"; 322 DCHECK(success) << "Failed to parse dismissed offline page ID from prefs";
200 dismissed_ids_.insert(std::move(dismissed_id)); 323 dismissed_ids.insert(dismissed_id);
201 } 324 }
325 return dismissed_ids;
202 } 326 }
203 327
204 void OfflinePageSuggestionsProvider::StoreDismissedIDsToPrefs() { 328 void OfflinePageSuggestionsProvider::StoreDismissedIDsToPrefs(
329 const std::string& pref_name,
330 const std::set<std::string>& dismissed_ids) const {
205 base::ListValue list; 331 base::ListValue list;
206 for (const std::string& dismissed_id : dismissed_ids_) 332 for (const std::string& dismissed_id : dismissed_ids)
207 list.AppendString(dismissed_id); 333 list.AppendString(dismissed_id);
208 pref_service_->Set(prefs::kDismissedOfflinePageSuggestions, list); 334 pref_service_->Set(pref_name, list);
209 } 335 }
210 336
211 } // namespace ntp_snippets 337 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698