15d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Copyright 2013 The Chromium Authors. All rights reserved.
25d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// found in the LICENSE file.
45d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
55d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/apps/ephemeral_app_service.h"
65d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
75f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include "apps/app_lifetime_monitor_factory.h"
85d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "base/command_line.h"
95f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include "base/message_loop/message_loop.h"
105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/apps/ephemeral_app_service_factory.h"
115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/chrome_notification_types.h"
125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/extensions/extension_service.h"
135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/extensions/extension_util.h"
145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/browser/profiles/profile.h"
155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "chrome/common/chrome_switches.h"
165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "content/public/browser/notification_service.h"
175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "content/public/browser/notification_source.h"
185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "content/public/browser/notification_types.h"
195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/browser/extension_prefs.h"
2023730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)#include "extensions/browser/extension_registry.h"
215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/browser/extension_system.h"
22cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)#include "extensions/browser/extension_util.h"
235f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include "extensions/browser/notification_types.h"
245f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)#include "extensions/browser/uninstall_reason.h"
255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/common/extension.h"
265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)#include "extensions/common/extension_set.h"
275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)using extensions::Extension;
295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)using extensions::ExtensionPrefs;
30116680a4aac90f2aa7413d9095a592090648e557Ben Murdochusing extensions::ExtensionRegistry;
315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)using extensions::ExtensionSet;
325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)using extensions::ExtensionSystem;
335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)namespace {
355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// The number of seconds after startup before performing garbage collection
375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// of ephemeral apps.
38effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochconst int kGarbageCollectAppsStartupDelay = 60;
395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// The number of seconds after an ephemeral app has been installed before
415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// performing garbage collection.
42effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochconst int kGarbageCollectAppsInstallDelay = 15;
435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
445d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// When the number of ephemeral apps reaches this count, trigger garbage
455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// collection to trim off the least-recently used apps in excess of
465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// kMaxEphemeralAppsCount.
47effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochconst int kGarbageCollectAppsTriggerCount = 35;
48effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
495f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)// The number of seconds after an app has stopped running before it will be
505f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)// disabled.
515f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)const int kDefaultDisableAppDelay = 1;
525f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)// The number of seconds after startup before disabling inactive ephemeral apps.
545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)const int kDisableAppsOnStartupDelay = 5;
555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}  // namespace
575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
585d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const int EphemeralAppService::kAppInactiveThreshold = 10;
595d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const int EphemeralAppService::kAppKeepThreshold = 1;
605d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)const int EphemeralAppService::kMaxEphemeralAppsCount = 30;
615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// static
635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)EphemeralAppService* EphemeralAppService::Get(Profile* profile) {
645d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  return EphemeralAppServiceFactory::GetForProfile(profile);
655d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)EphemeralAppService::EphemeralAppService(Profile* profile)
685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    : profile_(profile),
69cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      extension_registry_observer_(this),
705f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      app_lifetime_monitor_observer_(this),
715f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      ephemeral_app_count_(-1),
725f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      disable_idle_app_delay_(kDefaultDisableAppDelay),
735f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      weak_ptr_factory_(this) {
745f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  registrar_.Add(this,
755f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                 content::Source<Profile>(profile_));
775d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
795d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)EphemeralAppService::~EphemeralAppService() {
805d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
815d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
82116680a4aac90f2aa7413d9095a592090648e557Ben Murdochvoid EphemeralAppService::ClearCachedApps() {
83116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Cancel any pending garbage collects.
84116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  garbage_collect_apps_timer_.Stop();
85116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
86116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
87116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DCHECK(registry);
88116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
89116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DCHECK(prefs);
90116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ExtensionService* service =
91116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      ExtensionSystem::Get(profile_)->extension_service();
92116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DCHECK(service);
93116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
94116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  scoped_ptr<ExtensionSet> extensions =
95116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      registry->GenerateInstalledExtensionsSet();
96116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
97116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  for (ExtensionSet::const_iterator it = extensions->begin();
98116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch       it != extensions->end();
99116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch       ++it) {
100116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    std::string extension_id = (*it)->id();
101116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    if (!prefs->IsEphemeralApp(extension_id))
102116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      continue;
103116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
104116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    // Do not remove apps that are running.
105116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    if (!extensions::util::IsExtensionIdle(extension_id, profile_))
106116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      continue;
107116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
108116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    DCHECK(registry->GetExtensionById(extension_id,
109116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch                                      ExtensionRegistry::EVERYTHING));
110116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch    service->UninstallExtension(
111116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        extension_id,
1125f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
1135f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        base::Bind(&base::DoNothing),
114116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        NULL);
115116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  }
116116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch}
117116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
1185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::Observe(
1195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    int type,
1205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    const content::NotificationSource& source,
1215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    const content::NotificationDetails& details) {
1225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  switch (type) {
1235f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED: {
1245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      Init();
1255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      break;
1265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    }
1275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    default:
1285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      NOTREACHED();
129cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  }
130cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)}
131cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
132cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)void EphemeralAppService::OnExtensionWillBeInstalled(
133cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    content::BrowserContext* browser_context,
134cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const extensions::Extension* extension,
135cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    bool is_update,
136cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    bool from_ephemeral,
137cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    const std::string& old_name) {
138cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  if (from_ephemeral) {
139cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    // An ephemeral app was just promoted to a regular installed app.
140cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    --ephemeral_app_count_;
141cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    DCHECK_GE(ephemeral_app_count_, 0);
1425f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    HandleEphemeralAppPromoted(extension);
143cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  } else if (!is_update &&
144cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)             extensions::util::IsEphemeralApp(extension->id(), profile_)) {
1455f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    // A new ephemeral app was launched.
146cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    ++ephemeral_app_count_;
147cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    if (ephemeral_app_count_ >= kGarbageCollectAppsTriggerCount) {
148cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)      TriggerGarbageCollect(
149cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)          base::TimeDelta::FromSeconds(kGarbageCollectAppsInstallDelay));
150cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    }
151cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  }
152cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)}
153cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)
154cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)void EphemeralAppService::OnExtensionUninstalled(
155cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    content::BrowserContext* browser_context,
1565f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    const extensions::Extension* extension,
1575f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    extensions::UninstallReason reason) {
158cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  if (extensions::util::IsEphemeralApp(extension->id(), profile_)) {
159cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    --ephemeral_app_count_;
160cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    DCHECK_GE(ephemeral_app_count_, 0);
161cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  }
1625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
1635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
1645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)void EphemeralAppService::OnAppStop(Profile* profile,
1655f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                                    const std::string& app_id) {
1665f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  if (!extensions::util::IsEphemeralApp(app_id, profile_))
1675f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return;
1685f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1695f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  base::MessageLoop::current()->PostDelayedTask(
1705f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      FROM_HERE,
1715f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      base::Bind(&EphemeralAppService::DisableEphemeralApp,
1725f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 weak_ptr_factory_.GetWeakPtr(),
1735f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 app_id),
1745f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      base::TimeDelta::FromSeconds(disable_idle_app_delay_));
1755f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)}
1765f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1775f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)void EphemeralAppService::OnChromeTerminating() {
1785f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  garbage_collect_apps_timer_.Stop();
1795f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1805f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  extension_registry_observer_.RemoveAll();
1815f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  app_lifetime_monitor_observer_.RemoveAll();
1825f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)}
1835f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1845d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::Init() {
1855d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  InitEphemeralAppCount();
1865f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1875f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // Start observing.
1885f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
1895f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  app_lifetime_monitor_observer_.Add(
1905f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      apps::AppLifetimeMonitorFactory::GetForProfile(profile_));
1915f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1925f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // Execute startup clean up tasks (except during tests).
1935f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType))
1945f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return;
1955f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  TriggerGarbageCollect(
197effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      base::TimeDelta::FromSeconds(kGarbageCollectAppsStartupDelay));
1985f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1995f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  base::MessageLoop::current()->PostDelayedTask(
2005f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      FROM_HERE,
2015f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      base::Bind(&EphemeralAppService::DisableEphemeralAppsOnStartup,
2025f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                 weak_ptr_factory_.GetWeakPtr()),
2035f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      base::TimeDelta::FromSeconds(kDisableAppsOnStartupDelay));
2045d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
2055d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::InitEphemeralAppCount() {
2075d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  scoped_ptr<ExtensionSet> extensions =
208116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      ExtensionRegistry::Get(profile_)->GenerateInstalledExtensionsSet();
209cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
210cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  DCHECK(prefs);
2115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  ephemeral_app_count_ = 0;
2135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  for (ExtensionSet::const_iterator it = extensions->begin();
2145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)       it != extensions->end(); ++it) {
2151320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    const Extension* extension = it->get();
216cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    if (prefs->IsEphemeralApp(extension->id()))
2175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      ++ephemeral_app_count_;
2185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  }
2195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
2205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
2215f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)void EphemeralAppService::DisableEphemeralApp(const std::string& app_id) {
2225f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  if (!extensions::util::IsEphemeralApp(app_id, profile_) ||
2235f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      !extensions::util::IsExtensionIdle(app_id, profile_)) {
2245f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return;
2255f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  }
2265f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2275f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // After an ephemeral app has stopped running, unload it from extension
2285f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // system and disable it to prevent all background activity.
2295f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  ExtensionService* service =
2305f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      ExtensionSystem::Get(profile_)->extension_service();
2315f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  DCHECK(service);
2325f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  service->DisableExtension(app_id, Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
2335f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)}
2345f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2355f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)void EphemeralAppService::DisableEphemeralAppsOnStartup() {
2365f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
2375f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  DCHECK(prefs);
2385f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  ExtensionService* service =
2395f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      ExtensionSystem::Get(profile_)->extension_service();
2405f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  DCHECK(service);
2415f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2425f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // Ensure that all inactive ephemeral apps are disabled to prevent all
2435f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // background activity. This is done on startup to catch any apps that escaped
2445f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // being disabled on shutdown.
2455f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  scoped_ptr<ExtensionSet> extensions =
2465f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      ExtensionRegistry::Get(profile_)->GenerateInstalledExtensionsSet();
2475f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  for (ExtensionSet::const_iterator it = extensions->begin();
2485f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)       it != extensions->end();
2495f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)       ++it) {
2501320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    const Extension* extension = it->get();
2515f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if (!prefs->IsEphemeralApp(extension->id()))
2525f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      continue;
2535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    // Only V2 apps are installed ephemerally. Remove other ephemeral app types
2555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    // that were cached before this policy was introduced.
2565f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if (!extension->is_platform_app()) {
2575f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      service->UninstallExtension(
2585f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          extension->id(),
2595f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
2605f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          base::Bind(&base::DoNothing),
2615f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          NULL);
2625f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      continue;
2635f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    }
2645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2655f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if (!prefs->HasDisableReason(extension->id(),
2665f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                                 Extension::DISABLE_INACTIVE_EPHEMERAL_APP) &&
2675f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        !prefs->IsExtensionRunning(extension->id()) &&
2685f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        extensions::util::IsExtensionIdle(extension->id(), profile_)) {
2695f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      service->DisableExtension(extension->id(),
2705f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                                Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
2715f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    }
2725f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  }
2735f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)}
2745f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2755f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)void EphemeralAppService::HandleEphemeralAppPromoted(const Extension* app) {
2765f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // When ephemeral apps are promoted to regular install apps, remove the
2775f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // DISABLE_INACTIVE_EPHEMERAL_APP reason and enable the app if there are no
2785f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  // other reasons.
2795f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
2805f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  DCHECK(prefs);
2815f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2825f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  int disable_reasons = prefs->GetDisableReasons(app->id());
2835f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  if (disable_reasons & Extension::DISABLE_INACTIVE_EPHEMERAL_APP) {
2845f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    prefs->RemoveDisableReason(app->id(),
2855f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                               Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
2865f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if (disable_reasons == Extension::DISABLE_INACTIVE_EPHEMERAL_APP)
2875f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      prefs->SetExtensionState(app->id(), Extension::ENABLED);
2885f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  }
2895f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)}
2905f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::TriggerGarbageCollect(const base::TimeDelta& delay) {
292effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  if (!garbage_collect_apps_timer_.IsRunning()) {
293effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    garbage_collect_apps_timer_.Start(
294effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        FROM_HERE,
295effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        delay,
296effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        this,
297effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        &EphemeralAppService::GarbageCollectApps);
298effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  }
2995d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
3005d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3015d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::GarbageCollectApps() {
302116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
303116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  DCHECK(registry);
30423730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
305cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)  DCHECK(prefs);
3065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
307116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  scoped_ptr<ExtensionSet> extensions =
308116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      registry->GenerateInstalledExtensionsSet();
309116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
3105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  int app_count = 0;
3115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  LaunchTimeAppMap app_launch_times;
3125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  std::set<std::string> remove_app_ids;
3135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  // Populate a list of ephemeral apps, ordered by their last launch time.
3155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  for (ExtensionSet::const_iterator it = extensions->begin();
3165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)       it != extensions->end(); ++it) {
3171320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci    const Extension* extension = it->get();
318cedac228d2dd51db4b79ea1e72c7f249408ee061Torne (Richard Coles)    if (!prefs->IsEphemeralApp(extension->id()))
3195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      continue;
3205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    ++app_count;
3225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // Do not remove ephemeral apps that are running.
3245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if (!extensions::util::IsExtensionIdle(extension->id(), profile_))
3255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      continue;
3265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    base::Time last_launch_time = prefs->GetLastLaunchTime(extension->id());
3285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // If the last launch time is invalid, this may be because it was just
3305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // installed. So use the install time. If this is also null for some reason,
3315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // the app will be removed.
3325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if (last_launch_time.is_null())
3335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      last_launch_time = prefs->GetInstallTime(extension->id());
3345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    app_launch_times.insert(std::make_pair(last_launch_time, extension->id()));
3365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  }
3375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
33823730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)  ExtensionService* service =
33923730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)      ExtensionSystem::Get(profile_)->extension_service();
34023730a6e56a168d1879203e4b3819bb36e3d8f1fTorne (Richard Coles)  DCHECK(service);
341116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Execute the eviction policies and remove apps marked for deletion.
3425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  if (!app_launch_times.empty()) {
3435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    GetAppsToRemove(app_count, app_launch_times, &remove_app_ids);
344116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
3455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    for (std::set<std::string>::const_iterator id = remove_app_ids.begin();
3465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)         id != remove_app_ids.end(); ++id) {
347116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      // Protect against cascading uninstalls.
348116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      if (!registry->GetExtensionById(*id, ExtensionRegistry::EVERYTHING))
349116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch        continue;
350116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
351116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      service->UninstallExtension(
352116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch          *id,
3535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
3545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          base::Bind(&base::DoNothing),
355116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch          NULL);
3565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    }
3575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  }
3585d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
3595d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3605d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// static
3615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)void EphemeralAppService::GetAppsToRemove(
3625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    int app_count,
3635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    const LaunchTimeAppMap& app_launch_times,
3645d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    std::set<std::string>* remove_app_ids) {
3655d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  base::Time time_now = base::Time::Now();
3665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  const base::Time inactive_threshold =
3675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      time_now - base::TimeDelta::FromDays(kAppInactiveThreshold);
3685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  const base::Time keep_threshold =
3695d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      time_now - base::TimeDelta::FromDays(kAppKeepThreshold);
3705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  // Visit the apps in order of least recently to most recently launched.
3725d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  for (LaunchTimeAppMap::const_iterator it = app_launch_times.begin();
3735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)       it != app_launch_times.end(); ++it) {
3745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // Cannot remove apps that have been launched recently. So break when we
3755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // reach the new apps.
3765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if (it->first > keep_threshold)
3775d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)        break;
3785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)
3795d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // Remove ephemeral apps that have been inactive for a while or if the cache
3805d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    // is larger than the desired size.
3815d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    if (it->first < inactive_threshold || app_count > kMaxEphemeralAppsCount) {
3825d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      remove_app_ids->insert(it->second);
3835d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      --app_count;
3845d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    } else {
3855d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)      break;
3865d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    }
3875d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  }
3885d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)}
389