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

Side by Side Diff: content/browser/service_worker/service_worker_register_job.cc

Issue 360123002: ServiceWorker: some more groundwork in support of the update process (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 6 years, 5 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 | Annotate | Revision Log
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 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 "content/browser/service_worker/service_worker_register_job.h" 5 #include "content/browser/service_worker/service_worker_register_job.h"
6 6
7 #include <vector> 7 #include <vector>
8 8
9 #include "base/message_loop/message_loop.h" 9 #include "base/message_loop/message_loop.h"
10 #include "content/browser/service_worker/service_worker_context_core.h" 10 #include "content/browser/service_worker/service_worker_context_core.h"
(...skipping 29 matching lines...) Expand all
40 ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() { 40 ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() {
41 DCHECK(!context_ || 41 DCHECK(!context_ ||
42 phase_ == INITIAL || phase_ == COMPLETE || phase_ == ABORT) 42 phase_ == INITIAL || phase_ == COMPLETE || phase_ == ABORT)
43 << "Jobs should only be interrupted during shutdown."; 43 << "Jobs should only be interrupted during shutdown.";
44 } 44 }
45 45
46 void ServiceWorkerRegisterJob::AddCallback(const RegistrationCallback& callback, 46 void ServiceWorkerRegisterJob::AddCallback(const RegistrationCallback& callback,
47 int process_id) { 47 int process_id) {
48 if (!is_promise_resolved_) { 48 if (!is_promise_resolved_) {
49 callbacks_.push_back(callback); 49 callbacks_.push_back(callback);
50 if (process_id != -1 && (phase_ < UPDATE || !pending_version())) 50 if (process_id != -1 && (phase_ < UPDATE || !new_version()))
51 pending_process_ids_.push_back(process_id); 51 pending_process_ids_.push_back(process_id);
52 return; 52 return;
53 } 53 }
54 RunSoon(base::Bind( 54 RunSoon(base::Bind(
55 callback, promise_resolved_status_, 55 callback, promise_resolved_status_,
56 promise_resolved_registration_, promise_resolved_version_)); 56 promise_resolved_registration_, promise_resolved_version_));
57 } 57 }
58 58
59 void ServiceWorkerRegisterJob::Start() { 59 void ServiceWorkerRegisterJob::Start() {
60 SetPhase(START); 60 SetPhase(START);
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
94 DCHECK(phase_ == START || phase_ == REGISTER) << phase_; 94 DCHECK(phase_ == START || phase_ == REGISTER) << phase_;
95 DCHECK(!internal_.registration); 95 DCHECK(!internal_.registration);
96 internal_.registration = registration; 96 internal_.registration = registration;
97 } 97 }
98 98
99 ServiceWorkerRegistration* ServiceWorkerRegisterJob::registration() { 99 ServiceWorkerRegistration* ServiceWorkerRegisterJob::registration() {
100 DCHECK(phase_ >= REGISTER) << phase_; 100 DCHECK(phase_ >= REGISTER) << phase_;
101 return internal_.registration; 101 return internal_.registration;
102 } 102 }
103 103
104 void ServiceWorkerRegisterJob::set_pending_version( 104 void ServiceWorkerRegisterJob::set_new_version(
105 ServiceWorkerVersion* version) { 105 ServiceWorkerVersion* version) {
106 DCHECK(phase_ == UPDATE) << phase_; 106 DCHECK(phase_ == UPDATE) << phase_;
107 DCHECK(!internal_.pending_version); 107 DCHECK(!internal_.new_version);
108 internal_.pending_version = version; 108 internal_.new_version = version;
109 } 109 }
110 110
111 ServiceWorkerVersion* ServiceWorkerRegisterJob::pending_version() { 111 ServiceWorkerVersion* ServiceWorkerRegisterJob::new_version() {
112 DCHECK(phase_ >= UPDATE) << phase_; 112 DCHECK(phase_ >= UPDATE) << phase_;
113 return internal_.pending_version; 113 return internal_.new_version;
114 } 114 }
115 115
116 void ServiceWorkerRegisterJob::SetPhase(Phase phase) { 116 void ServiceWorkerRegisterJob::SetPhase(Phase phase) {
117 switch (phase) { 117 switch (phase) {
118 case INITIAL: 118 case INITIAL:
119 NOTREACHED(); 119 NOTREACHED();
120 break; 120 break;
121 case START: 121 case START:
122 DCHECK(phase_ == INITIAL) << phase_; 122 DCHECK(phase_ == INITIAL) << phase_;
123 break; 123 break;
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after
223 // Abort this registration job. 223 // Abort this registration job.
224 Complete(status); 224 Complete(status);
225 return; 225 return;
226 } 226 }
227 227
228 // TODO(falken): "If serviceWorkerRegistration.installingWorker is not null.." 228 // TODO(falken): "If serviceWorkerRegistration.installingWorker is not null.."
229 // then terminate the installing worker. It doesn't make sense to implement 229 // then terminate the installing worker. It doesn't make sense to implement
230 // yet since we always activate the worker if install completed, so there can 230 // yet since we always activate the worker if install completed, so there can
231 // be no installing worker at this point. 231 // be no installing worker at this point.
232 // TODO(nhiroki): Check 'installing_version()' instead when it's supported. 232 // TODO(nhiroki): Check 'installing_version()' instead when it's supported.
233 DCHECK(!registration()->waiting_version());
234 233
235 // "Let serviceWorker be a newly-created ServiceWorker object..." and start 234 // "Let serviceWorker be a newly-created ServiceWorker object..." and start
236 // the worker. 235 // the worker.
237 set_pending_version(new ServiceWorkerVersion( 236 set_new_version(new ServiceWorkerVersion(
238 registration(), context_->storage()->NewVersionId(), context_)); 237 registration(), context_->storage()->NewVersionId(), context_));
239 238
240 // TODO(michaeln): Pause after downloading when performing an update. 239 // TODO(michaeln): Pause after downloading when performing an update.
241 bool pause_after_download = false; 240 bool pause_after_download = false;
242 if (pause_after_download) 241 if (pause_after_download)
243 pending_version()->embedded_worker()->AddListener(this); 242 new_version()->embedded_worker()->AddListener(this);
244 pending_version()->StartWorkerWithCandidateProcesses( 243 new_version()->StartWorkerWithCandidateProcesses(
245 pending_process_ids_, 244 pending_process_ids_,
246 pause_after_download, 245 pause_after_download,
247 base::Bind(&ServiceWorkerRegisterJob::OnStartWorkerFinished, 246 base::Bind(&ServiceWorkerRegisterJob::OnStartWorkerFinished,
248 weak_factory_.GetWeakPtr())); 247 weak_factory_.GetWeakPtr()));
249 } 248 }
250 249
251 void ServiceWorkerRegisterJob::OnStartWorkerFinished( 250 void ServiceWorkerRegisterJob::OnStartWorkerFinished(
252 ServiceWorkerStatusCode status) { 251 ServiceWorkerStatusCode status) {
253 // "If serviceWorker fails to start up..." then reject the promise with an 252 // "If serviceWorker fails to start up..." then reject the promise with an
254 // error and abort. 253 // error and abort.
255 if (status != SERVICE_WORKER_OK) { 254 if (status != SERVICE_WORKER_OK) {
256 Complete(status); 255 Complete(status);
257 return; 256 return;
258 } 257 }
259 258
260 // "Resolve promise with serviceWorker." 259 // "Resolve promise with serviceWorker."
261 // Although the spec doesn't set waitingWorker until after resolving the 260 DCHECK(!registration()->installing_version());
262 // promise, our system's resolving works by passing ServiceWorkerRegistration 261 ResolvePromise(status, registration(), new_version());
263 // to the callbacks, so waitingWorker must be set first. 262 registration()->SetInstallingVersion(new_version());
264 DCHECK(!registration()->waiting_version()); 263 AssociateInstallingVersionToDocuments(context_, new_version());
265 registration()->set_waiting_version(pending_version());
266 ResolvePromise(status, registration(), pending_version());
267
268 AssociateWaitingVersionToDocuments(context_, pending_version());
269
270 InstallAndContinue(); 264 InstallAndContinue();
271 } 265 }
272 266
273 // This function corresponds to the spec's _Install algorithm. 267 // This function corresponds to the spec's _Install algorithm.
274 void ServiceWorkerRegisterJob::InstallAndContinue() { 268 void ServiceWorkerRegisterJob::InstallAndContinue() {
275 SetPhase(INSTALL); 269 SetPhase(INSTALL);
276 // "Set serviceWorkerRegistration.installingWorker._state to installing." 270 // "Set serviceWorkerRegistration.installingWorker._state to installing."
277 // "Fire install event on the associated ServiceWorkerGlobalScope object." 271 // "Fire install event on the associated ServiceWorkerGlobalScope object."
278 pending_version()->DispatchInstallEvent( 272 new_version()->DispatchInstallEvent(
279 -1, 273 -1,
280 base::Bind(&ServiceWorkerRegisterJob::OnInstallFinished, 274 base::Bind(&ServiceWorkerRegisterJob::OnInstallFinished,
281 weak_factory_.GetWeakPtr())); 275 weak_factory_.GetWeakPtr()));
282 } 276 }
283 277
284 void ServiceWorkerRegisterJob::OnInstallFinished( 278 void ServiceWorkerRegisterJob::OnInstallFinished(
285 ServiceWorkerStatusCode status) { 279 ServiceWorkerStatusCode status) {
286 // "If any handler called waitUntil()..." and the resulting promise 280 // "If any handler called waitUntil()..." and the resulting promise
287 // is rejected, abort. 281 // is rejected, abort.
288 // TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is 282 // TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is
289 // unexpectedly terminated) we may want to retry sending the event again. 283 // unexpectedly terminated) we may want to retry sending the event again.
290 if (status != SERVICE_WORKER_OK) { 284 if (status != SERVICE_WORKER_OK) {
291 Complete(status); 285 Complete(status);
292 return; 286 return;
293 } 287 }
294 288
295 SetPhase(STORE); 289 SetPhase(STORE);
296 context_->storage()->StoreRegistration( 290 context_->storage()->StoreRegistration(
297 registration(), 291 registration(),
298 pending_version(), 292 new_version(),
299 base::Bind(&ServiceWorkerRegisterJob::OnStoreRegistrationComplete, 293 base::Bind(&ServiceWorkerRegisterJob::OnStoreRegistrationComplete,
300 weak_factory_.GetWeakPtr())); 294 weak_factory_.GetWeakPtr()));
301 } 295 }
302 296
303 void ServiceWorkerRegisterJob::OnStoreRegistrationComplete( 297 void ServiceWorkerRegisterJob::OnStoreRegistrationComplete(
304 ServiceWorkerStatusCode status) { 298 ServiceWorkerStatusCode status) {
305 if (status != SERVICE_WORKER_OK) { 299 if (status != SERVICE_WORKER_OK) {
306 Complete(status); 300 Complete(status);
307 return; 301 return;
308 } 302 }
309 303 registration()->SetWaitingVersion(new_version());
310 ActivateAndContinue(); 304 ActivateAndContinue();
311 } 305 }
312 306
313 // This function corresponds to the spec's _Activate algorithm. 307 // This function corresponds to the spec's _Activate algorithm.
314 void ServiceWorkerRegisterJob::ActivateAndContinue() { 308 void ServiceWorkerRegisterJob::ActivateAndContinue() {
315 SetPhase(ACTIVATE); 309 SetPhase(ACTIVATE);
316 310
317 // "If existingWorker is not null, then: wait for exitingWorker to finish 311 // "If existingWorker is not null, then: wait for exitingWorker to finish
318 // handling any in-progress requests." 312 // handling any in-progress requests."
319 // See if we already have an active_version for the scope and it has 313 // See if we already have an active_version for the scope and it has
320 // controllee documents (if so activating the new version should wait 314 // controllee documents (if so activating the new version should wait
321 // until we have no documents controlled by the version). 315 // until we have no documents controlled by the version).
322 if (registration()->active_version() && 316 if (registration()->active_version() &&
323 registration()->active_version()->HasControllee()) { 317 registration()->active_version()->HasControllee()) {
324 // TODO(kinuko,falken): Currently we immediately return if the existing 318 // TODO(kinuko,falken): Currently we immediately return if the existing
325 // registration already has an active version, so we shouldn't come 319 // registration already has an active version, so we shouldn't come
326 // this way. 320 // this way.
327 NOTREACHED(); 321 NOTREACHED();
328 // TODO(falken): Register an continuation task to wait for NoControllees 322 // TODO(falken): Register an continuation task to wait for NoControllees
329 // notification so that we can resume activation later (see comments 323 // notification so that we can resume activation later (see comments
330 // in ServiceWorkerVersion::RemoveControllee). 324 // in ServiceWorkerVersion::RemoveControllee).
331 Complete(SERVICE_WORKER_OK); 325 Complete(SERVICE_WORKER_OK);
332 return; 326 return;
333 } 327 }
334 328
335 // "Set serviceWorkerRegistration.waitingWorker to null." 329 // "Set serviceWorkerRegistration.waitingWorker to null."
336 // "Set serviceWorkerRegistration.activeWorker to activatingWorker." 330 // "Set serviceWorkerRegistration.activeWorker to activatingWorker."
337 DisassociateWaitingVersionFromDocuments( 331 registration()->SetActiveVersion(new_version());
338 context_, pending_version()->version_id());
339 registration()->set_waiting_version(NULL);
340 DCHECK(!registration()->active_version());
341 registration()->set_active_version(pending_version());
342 332
343 // "Set serviceWorkerRegistration.activeWorker._state to activating." 333 // "Set serviceWorkerRegistration.activeWorker._state to activating."
344 // "Fire activate event on the associated ServiceWorkerGlobalScope object." 334 // "Fire activate event on the associated ServiceWorkerGlobalScope object."
345 // "Set serviceWorkerRegistration.activeWorker._state to active." 335 // "Set serviceWorkerRegistration.activeWorker._state to active."
346 pending_version()->DispatchActivateEvent( 336 new_version()->DispatchActivateEvent(
347 base::Bind(&ServiceWorkerRegisterJob::OnActivateFinished, 337 base::Bind(&ServiceWorkerRegisterJob::OnActivateFinished,
348 weak_factory_.GetWeakPtr())); 338 weak_factory_.GetWeakPtr()));
349 } 339 }
350 340
351 void ServiceWorkerRegisterJob::OnActivateFinished( 341 void ServiceWorkerRegisterJob::OnActivateFinished(
352 ServiceWorkerStatusCode status) { 342 ServiceWorkerStatusCode status) {
353 // "If any handler called waitUntil()..." and the resulting promise 343 // "If any handler called waitUntil()..." and the resulting promise
354 // is rejected, abort. 344 // is rejected, abort.
355 // TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is 345 // TODO(kinuko,falken): For some error cases (e.g. ServiceWorker is
356 // unexpectedly terminated) we may want to retry sending the event again. 346 // unexpectedly terminated) we may want to retry sending the event again.
357 if (status != SERVICE_WORKER_OK) { 347 if (status != SERVICE_WORKER_OK) {
358 registration()->set_active_version(NULL); 348 registration()->SetActiveVersion(NULL);
359 Complete(status); 349 Complete(status);
360 return; 350 return;
361 } 351 }
362 context_->storage()->UpdateToActiveState( 352 context_->storage()->UpdateToActiveState(
363 registration(), 353 registration(),
364 base::Bind(&ServiceWorkerUtils::NoOpStatusCallback)); 354 base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
365 Complete(SERVICE_WORKER_OK); 355 Complete(SERVICE_WORKER_OK);
366 } 356 }
367 357
368 void ServiceWorkerRegisterJob::Complete(ServiceWorkerStatusCode status) { 358 void ServiceWorkerRegisterJob::Complete(ServiceWorkerStatusCode status) {
369 CompleteInternal(status); 359 CompleteInternal(status);
370 context_->job_coordinator()->FinishJob(pattern_, this); 360 context_->job_coordinator()->FinishJob(pattern_, this);
371 } 361 }
372 362
373 void ServiceWorkerRegisterJob::CompleteInternal( 363 void ServiceWorkerRegisterJob::CompleteInternal(
374 ServiceWorkerStatusCode status) { 364 ServiceWorkerStatusCode status) {
375 SetPhase(COMPLETE); 365 SetPhase(COMPLETE);
376 if (status != SERVICE_WORKER_OK) { 366 if (status != SERVICE_WORKER_OK) {
377 if (registration() && registration()->waiting_version()) { 367 if (registration()) {
378 DisassociateWaitingVersionFromDocuments( 368 if (new_version()) {
379 context_, registration()->waiting_version()->version_id()); 369 DisassociateVersionFromDocuments(context_, new_version());
380 registration()->set_waiting_version(NULL); 370 registration()->UnsetVersion(new_version());
381 } 371 }
382 if (registration() && !registration()->active_version()) { 372 if (!registration()->active_version()) {
383 context_->storage()->DeleteRegistration( 373 context_->storage()->DeleteRegistration(
384 registration()->id(), 374 registration()->id(),
385 registration()->script_url().GetOrigin(), 375 registration()->script_url().GetOrigin(),
386 base::Bind(&ServiceWorkerUtils::NoOpStatusCallback)); 376 base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
377 }
387 } 378 }
388 if (!is_promise_resolved_) 379 if (!is_promise_resolved_)
389 ResolvePromise(status, NULL, NULL); 380 ResolvePromise(status, NULL, NULL);
390 } 381 }
391 DCHECK(callbacks_.empty()); 382 DCHECK(callbacks_.empty());
392 if (registration()) { 383 if (registration()) {
393 context_->storage()->NotifyDoneInstallingRegistration( 384 context_->storage()->NotifyDoneInstallingRegistration(
394 registration(), pending_version(), status); 385 registration(), new_version(), status);
395 } 386 }
396 if (pending_version()) 387 if (new_version())
397 pending_version()->embedded_worker()->RemoveListener(this); 388 new_version()->embedded_worker()->RemoveListener(this);
398 } 389 }
399 390
400 void ServiceWorkerRegisterJob::ResolvePromise( 391 void ServiceWorkerRegisterJob::ResolvePromise(
401 ServiceWorkerStatusCode status, 392 ServiceWorkerStatusCode status,
402 ServiceWorkerRegistration* registration, 393 ServiceWorkerRegistration* registration,
403 ServiceWorkerVersion* version) { 394 ServiceWorkerVersion* version) {
404 DCHECK(!is_promise_resolved_); 395 DCHECK(!is_promise_resolved_);
405 is_promise_resolved_ = true; 396 is_promise_resolved_ = true;
406 promise_resolved_status_ = status; 397 promise_resolved_status_ = status;
407 promise_resolved_registration_ = registration; 398 promise_resolved_registration_ = registration;
408 promise_resolved_version_ = version; 399 promise_resolved_version_ = version;
409 for (std::vector<RegistrationCallback>::iterator it = callbacks_.begin(); 400 for (std::vector<RegistrationCallback>::iterator it = callbacks_.begin();
410 it != callbacks_.end(); 401 it != callbacks_.end();
411 ++it) { 402 ++it) {
412 it->Run(status, registration, version); 403 it->Run(status, registration, version);
413 } 404 }
414 callbacks_.clear(); 405 callbacks_.clear();
415 } 406 }
416 407
417 void ServiceWorkerRegisterJob::OnPausedAfterDownload() { 408 void ServiceWorkerRegisterJob::OnPausedAfterDownload() {
418 // TODO(michaeln): Compare the old and new script. 409 // TODO(michaeln): Compare the old and new script.
419 // If different unpause the worker and continue with 410 // If different unpause the worker and continue with
420 // the job. If the same ResolvePromise with the current 411 // the job. If the same ResolvePromise with the current
421 // version and complete the job, throwing away the new version 412 // version and complete the job, throwing away the new version
422 // since there's nothing new. 413 // since there's nothing new.
423 pending_version()->embedded_worker()->RemoveListener(this); 414 new_version()->embedded_worker()->RemoveListener(this);
424 pending_version()->embedded_worker()->ResumeAfterDownload(); 415 new_version()->embedded_worker()->ResumeAfterDownload();
425 } 416 }
426 417
427 bool ServiceWorkerRegisterJob::OnMessageReceived(const IPC::Message& message) { 418 bool ServiceWorkerRegisterJob::OnMessageReceived(const IPC::Message& message) {
428 return false; 419 return false;
429 } 420 }
430 421
431 // static 422 // static
432 void ServiceWorkerRegisterJob::AssociateWaitingVersionToDocuments( 423 void ServiceWorkerRegisterJob::AssociateInstallingVersionToDocuments(
433 base::WeakPtr<ServiceWorkerContextCore> context, 424 base::WeakPtr<ServiceWorkerContextCore> context,
434 ServiceWorkerVersion* version) { 425 ServiceWorkerVersion* version) {
435 DCHECK(context); 426 DCHECK(context);
436 DCHECK(version); 427 DCHECK(version);
437 428
438 for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it = 429 for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it =
439 context->GetProviderHostIterator(); 430 context->GetProviderHostIterator();
440 !it->IsAtEnd(); 431 !it->IsAtEnd(); it->Advance()) {
441 it->Advance()) {
442 ServiceWorkerProviderHost* host = it->GetProviderHost(); 432 ServiceWorkerProviderHost* host = it->GetProviderHost();
443 if (!host->IsContextAlive())
444 continue;
445 if (ServiceWorkerUtils::ScopeMatches(version->scope(), 433 if (ServiceWorkerUtils::ScopeMatches(version->scope(),
446 host->document_url())) { 434 host->document_url())) {
447 // The spec's _Update algorithm says, "upgrades active version to a new 435 if (!host->CanAssociateVersion(version))
448 // version for the same URL scope.", so skip if the scope (registration)
449 // of |version| is different from that of the current active/waiting
450 // version.
451 if (!host->ValidateVersionForAssociation(version))
452 continue; 436 continue;
453 437 host->SetInstallingVersion(version);
454 // TODO(nhiroki): Keep |host->waiting_version()| to be replaced and set
455 // status of them to 'redandunt' after breaking the loop.
456
457 host->SetWaitingVersion(version);
458 // TODO(nhiroki): Set |host|'s installing version to null.
459 } 438 }
460 } 439 }
461 } 440 }
462 441
463 // static 442 // static
464 void ServiceWorkerRegisterJob::DisassociateWaitingVersionFromDocuments( 443 void ServiceWorkerRegisterJob::DisassociateVersionFromDocuments(
465 base::WeakPtr<ServiceWorkerContextCore> context, 444 base::WeakPtr<ServiceWorkerContextCore> context,
466 int64 version_id) { 445 ServiceWorkerVersion* version) {
467 DCHECK(context); 446 DCHECK(context);
468 for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it = 447 for (scoped_ptr<ServiceWorkerContextCore::ProviderHostIterator> it =
469 context->GetProviderHostIterator(); 448 context->GetProviderHostIterator();
470 !it->IsAtEnd(); 449 !it->IsAtEnd(); it->Advance()) {
471 it->Advance()) {
472 ServiceWorkerProviderHost* host = it->GetProviderHost(); 450 ServiceWorkerProviderHost* host = it->GetProviderHost();
473 if (!host->IsContextAlive()) 451 host->UnsetVersion(version);
474 continue;
475 if (host->waiting_version() &&
476 host->waiting_version()->version_id() == version_id) {
477 host->SetWaitingVersion(NULL);
478 }
479 } 452 }
480 } 453 }
481 454
482 } // namespace content 455 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698