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
3// found in the LICENSE file.
4
5#include "chrome/browser/apps/ephemeral_app_service.h"
6
7#include "base/command_line.h"
8#include "chrome/browser/apps/ephemeral_app_service_factory.h"
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/extensions/extension_service.h"
11#include "chrome/browser/extensions/extension_util.h"
12#include "chrome/browser/profiles/profile.h"
13#include "chrome/common/chrome_switches.h"
14#include "content/public/browser/notification_service.h"
15#include "content/public/browser/notification_source.h"
16#include "content/public/browser/notification_types.h"
17#include "extensions/browser/extension_prefs.h"
18#include "extensions/browser/extension_registry.h"
19#include "extensions/browser/extension_system.h"
20#include "extensions/browser/extension_util.h"
21#include "extensions/common/extension.h"
22#include "extensions/common/extension_set.h"
23
24using extensions::Extension;
25using extensions::ExtensionPrefs;
26using extensions::ExtensionSet;
27using extensions::ExtensionSystem;
28
29namespace {
30
31// The number of seconds after startup before performing garbage collection
32// of ephemeral apps.
33const int kGarbageCollectAppsStartupDelay = 60;
34
35// The number of seconds after an ephemeral app has been installed before
36// performing garbage collection.
37const int kGarbageCollectAppsInstallDelay = 15;
38
39// When the number of ephemeral apps reaches this count, trigger garbage
40// collection to trim off the least-recently used apps in excess of
41// kMaxEphemeralAppsCount.
42const int kGarbageCollectAppsTriggerCount = 35;
43
44}  // namespace
45
46const int EphemeralAppService::kAppInactiveThreshold = 10;
47const int EphemeralAppService::kAppKeepThreshold = 1;
48const int EphemeralAppService::kMaxEphemeralAppsCount = 30;
49
50// static
51EphemeralAppService* EphemeralAppService::Get(Profile* profile) {
52  return EphemeralAppServiceFactory::GetForProfile(profile);
53}
54
55EphemeralAppService::EphemeralAppService(Profile* profile)
56    : profile_(profile),
57      extension_registry_observer_(this),
58      ephemeral_app_count_(-1) {
59  if (!CommandLine::ForCurrentProcess()->HasSwitch(
60            switches::kEnableEphemeralApps))
61    return;
62
63  extension_registry_observer_.Add(
64      extensions::ExtensionRegistry::Get(profile_));
65  registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
66                 content::Source<Profile>(profile_));
67  registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
68                 content::Source<Profile>(profile_));
69}
70
71EphemeralAppService::~EphemeralAppService() {
72}
73
74void EphemeralAppService::Observe(
75    int type,
76    const content::NotificationSource& source,
77    const content::NotificationDetails& details) {
78  switch (type) {
79    case chrome::NOTIFICATION_EXTENSIONS_READY: {
80      Init();
81      break;
82    }
83    case chrome::NOTIFICATION_PROFILE_DESTROYED: {
84      // Ideally we need to know when the extension system is shutting down.
85      garbage_collect_apps_timer_.Stop();
86      break;
87    }
88    default:
89      NOTREACHED();
90  }
91}
92
93void EphemeralAppService::OnExtensionWillBeInstalled(
94    content::BrowserContext* browser_context,
95    const extensions::Extension* extension,
96    bool is_update,
97    bool from_ephemeral,
98    const std::string& old_name) {
99  if (from_ephemeral) {
100    // An ephemeral app was just promoted to a regular installed app.
101    --ephemeral_app_count_;
102    DCHECK_GE(ephemeral_app_count_, 0);
103  } else if (!is_update &&
104             extensions::util::IsEphemeralApp(extension->id(), profile_)) {
105    ++ephemeral_app_count_;
106    if (ephemeral_app_count_ >= kGarbageCollectAppsTriggerCount) {
107      TriggerGarbageCollect(
108          base::TimeDelta::FromSeconds(kGarbageCollectAppsInstallDelay));
109    }
110  }
111}
112
113void EphemeralAppService::OnExtensionUninstalled(
114    content::BrowserContext* browser_context,
115    const extensions::Extension* extension) {
116  if (extensions::util::IsEphemeralApp(extension->id(), profile_)) {
117    --ephemeral_app_count_;
118    DCHECK_GE(ephemeral_app_count_, 0);
119  }
120}
121
122void EphemeralAppService::Init() {
123  InitEphemeralAppCount();
124  TriggerGarbageCollect(
125      base::TimeDelta::FromSeconds(kGarbageCollectAppsStartupDelay));
126}
127
128void EphemeralAppService::InitEphemeralAppCount() {
129  scoped_ptr<ExtensionSet> extensions =
130      extensions::ExtensionRegistry::Get(profile_)
131          ->GenerateInstalledExtensionsSet();
132  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
133  DCHECK(prefs);
134
135  ephemeral_app_count_ = 0;
136  for (ExtensionSet::const_iterator it = extensions->begin();
137       it != extensions->end(); ++it) {
138    const Extension* extension = *it;
139    if (prefs->IsEphemeralApp(extension->id()))
140      ++ephemeral_app_count_;
141  }
142}
143
144void EphemeralAppService::TriggerGarbageCollect(const base::TimeDelta& delay) {
145  if (!garbage_collect_apps_timer_.IsRunning()) {
146    garbage_collect_apps_timer_.Start(
147        FROM_HERE,
148        delay,
149        this,
150        &EphemeralAppService::GarbageCollectApps);
151  }
152}
153
154void EphemeralAppService::GarbageCollectApps() {
155  scoped_ptr<ExtensionSet> extensions =
156      extensions::ExtensionRegistry::Get(profile_)
157          ->GenerateInstalledExtensionsSet();
158  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
159  DCHECK(prefs);
160
161  int app_count = 0;
162  LaunchTimeAppMap app_launch_times;
163  std::set<std::string> remove_app_ids;
164
165  // Populate a list of ephemeral apps, ordered by their last launch time.
166  for (ExtensionSet::const_iterator it = extensions->begin();
167       it != extensions->end(); ++it) {
168    const Extension* extension = *it;
169    if (!prefs->IsEphemeralApp(extension->id()))
170      continue;
171
172    ++app_count;
173
174    // Do not remove ephemeral apps that are running.
175    if (!extensions::util::IsExtensionIdle(extension->id(), profile_))
176      continue;
177
178    base::Time last_launch_time = prefs->GetLastLaunchTime(extension->id());
179
180    // If the last launch time is invalid, this may be because it was just
181    // installed. So use the install time. If this is also null for some reason,
182    // the app will be removed.
183    if (last_launch_time.is_null())
184      last_launch_time = prefs->GetInstallTime(extension->id());
185
186    app_launch_times.insert(std::make_pair(last_launch_time, extension->id()));
187  }
188
189  ExtensionService* service =
190      ExtensionSystem::Get(profile_)->extension_service();
191  DCHECK(service);
192  // Execute the replacement policies and remove apps marked for deletion.
193  if (!app_launch_times.empty()) {
194    GetAppsToRemove(app_count, app_launch_times, &remove_app_ids);
195    for (std::set<std::string>::const_iterator id = remove_app_ids.begin();
196         id != remove_app_ids.end(); ++id) {
197      if (service->UninstallExtension(*id, false, NULL))
198        --app_count;
199    }
200  }
201
202  ephemeral_app_count_ = app_count;
203}
204
205// static
206void EphemeralAppService::GetAppsToRemove(
207    int app_count,
208    const LaunchTimeAppMap& app_launch_times,
209    std::set<std::string>* remove_app_ids) {
210  base::Time time_now = base::Time::Now();
211  const base::Time inactive_threshold =
212      time_now - base::TimeDelta::FromDays(kAppInactiveThreshold);
213  const base::Time keep_threshold =
214      time_now - base::TimeDelta::FromDays(kAppKeepThreshold);
215
216  // Visit the apps in order of least recently to most recently launched.
217  for (LaunchTimeAppMap::const_iterator it = app_launch_times.begin();
218       it != app_launch_times.end(); ++it) {
219    // Cannot remove apps that have been launched recently. So break when we
220    // reach the new apps.
221    if (it->first > keep_threshold)
222        break;
223
224    // Remove ephemeral apps that have been inactive for a while or if the cache
225    // is larger than the desired size.
226    if (it->first < inactive_threshold || app_count > kMaxEphemeralAppsCount) {
227      remove_app_ids->insert(it->second);
228      --app_count;
229    } else {
230      break;
231    }
232  }
233}
234