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 "apps/app_lifetime_monitor_factory.h"
8#include "base/command_line.h"
9#include "base/message_loop/message_loop.h"
10#include "chrome/browser/apps/ephemeral_app_service_factory.h"
11#include "chrome/browser/chrome_notification_types.h"
12#include "chrome/browser/extensions/extension_service.h"
13#include "chrome/browser/extensions/extension_util.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/common/chrome_switches.h"
16#include "content/public/browser/notification_service.h"
17#include "content/public/browser/notification_source.h"
18#include "content/public/browser/notification_types.h"
19#include "extensions/browser/extension_prefs.h"
20#include "extensions/browser/extension_registry.h"
21#include "extensions/browser/extension_system.h"
22#include "extensions/browser/extension_util.h"
23#include "extensions/browser/notification_types.h"
24#include "extensions/browser/uninstall_reason.h"
25#include "extensions/common/extension.h"
26#include "extensions/common/extension_set.h"
27
28using extensions::Extension;
29using extensions::ExtensionPrefs;
30using extensions::ExtensionRegistry;
31using extensions::ExtensionSet;
32using extensions::ExtensionSystem;
33
34namespace {
35
36// The number of seconds after startup before performing garbage collection
37// of ephemeral apps.
38const int kGarbageCollectAppsStartupDelay = 60;
39
40// The number of seconds after an ephemeral app has been installed before
41// performing garbage collection.
42const int kGarbageCollectAppsInstallDelay = 15;
43
44// When the number of ephemeral apps reaches this count, trigger garbage
45// collection to trim off the least-recently used apps in excess of
46// kMaxEphemeralAppsCount.
47const int kGarbageCollectAppsTriggerCount = 35;
48
49// The number of seconds after an app has stopped running before it will be
50// disabled.
51const int kDefaultDisableAppDelay = 1;
52
53// The number of seconds after startup before disabling inactive ephemeral apps.
54const int kDisableAppsOnStartupDelay = 5;
55
56}  // namespace
57
58const int EphemeralAppService::kAppInactiveThreshold = 10;
59const int EphemeralAppService::kAppKeepThreshold = 1;
60const int EphemeralAppService::kMaxEphemeralAppsCount = 30;
61
62// static
63EphemeralAppService* EphemeralAppService::Get(Profile* profile) {
64  return EphemeralAppServiceFactory::GetForProfile(profile);
65}
66
67EphemeralAppService::EphemeralAppService(Profile* profile)
68    : profile_(profile),
69      extension_registry_observer_(this),
70      app_lifetime_monitor_observer_(this),
71      ephemeral_app_count_(-1),
72      disable_idle_app_delay_(kDefaultDisableAppDelay),
73      weak_ptr_factory_(this) {
74  registrar_.Add(this,
75                 extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED,
76                 content::Source<Profile>(profile_));
77}
78
79EphemeralAppService::~EphemeralAppService() {
80}
81
82void EphemeralAppService::ClearCachedApps() {
83  // Cancel any pending garbage collects.
84  garbage_collect_apps_timer_.Stop();
85
86  ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
87  DCHECK(registry);
88  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
89  DCHECK(prefs);
90  ExtensionService* service =
91      ExtensionSystem::Get(profile_)->extension_service();
92  DCHECK(service);
93
94  scoped_ptr<ExtensionSet> extensions =
95      registry->GenerateInstalledExtensionsSet();
96
97  for (ExtensionSet::const_iterator it = extensions->begin();
98       it != extensions->end();
99       ++it) {
100    std::string extension_id = (*it)->id();
101    if (!prefs->IsEphemeralApp(extension_id))
102      continue;
103
104    // Do not remove apps that are running.
105    if (!extensions::util::IsExtensionIdle(extension_id, profile_))
106      continue;
107
108    DCHECK(registry->GetExtensionById(extension_id,
109                                      ExtensionRegistry::EVERYTHING));
110    service->UninstallExtension(
111        extension_id,
112        extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
113        base::Bind(&base::DoNothing),
114        NULL);
115  }
116}
117
118void EphemeralAppService::Observe(
119    int type,
120    const content::NotificationSource& source,
121    const content::NotificationDetails& details) {
122  switch (type) {
123    case extensions::NOTIFICATION_EXTENSIONS_READY_DEPRECATED: {
124      Init();
125      break;
126    }
127    default:
128      NOTREACHED();
129  }
130}
131
132void EphemeralAppService::OnExtensionWillBeInstalled(
133    content::BrowserContext* browser_context,
134    const extensions::Extension* extension,
135    bool is_update,
136    bool from_ephemeral,
137    const std::string& old_name) {
138  if (from_ephemeral) {
139    // An ephemeral app was just promoted to a regular installed app.
140    --ephemeral_app_count_;
141    DCHECK_GE(ephemeral_app_count_, 0);
142    HandleEphemeralAppPromoted(extension);
143  } else if (!is_update &&
144             extensions::util::IsEphemeralApp(extension->id(), profile_)) {
145    // A new ephemeral app was launched.
146    ++ephemeral_app_count_;
147    if (ephemeral_app_count_ >= kGarbageCollectAppsTriggerCount) {
148      TriggerGarbageCollect(
149          base::TimeDelta::FromSeconds(kGarbageCollectAppsInstallDelay));
150    }
151  }
152}
153
154void EphemeralAppService::OnExtensionUninstalled(
155    content::BrowserContext* browser_context,
156    const extensions::Extension* extension,
157    extensions::UninstallReason reason) {
158  if (extensions::util::IsEphemeralApp(extension->id(), profile_)) {
159    --ephemeral_app_count_;
160    DCHECK_GE(ephemeral_app_count_, 0);
161  }
162}
163
164void EphemeralAppService::OnAppStop(Profile* profile,
165                                    const std::string& app_id) {
166  if (!extensions::util::IsEphemeralApp(app_id, profile_))
167    return;
168
169  base::MessageLoop::current()->PostDelayedTask(
170      FROM_HERE,
171      base::Bind(&EphemeralAppService::DisableEphemeralApp,
172                 weak_ptr_factory_.GetWeakPtr(),
173                 app_id),
174      base::TimeDelta::FromSeconds(disable_idle_app_delay_));
175}
176
177void EphemeralAppService::OnChromeTerminating() {
178  garbage_collect_apps_timer_.Stop();
179
180  extension_registry_observer_.RemoveAll();
181  app_lifetime_monitor_observer_.RemoveAll();
182}
183
184void EphemeralAppService::Init() {
185  InitEphemeralAppCount();
186
187  // Start observing.
188  extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
189  app_lifetime_monitor_observer_.Add(
190      apps::AppLifetimeMonitorFactory::GetForProfile(profile_));
191
192  // Execute startup clean up tasks (except during tests).
193  if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType))
194    return;
195
196  TriggerGarbageCollect(
197      base::TimeDelta::FromSeconds(kGarbageCollectAppsStartupDelay));
198
199  base::MessageLoop::current()->PostDelayedTask(
200      FROM_HERE,
201      base::Bind(&EphemeralAppService::DisableEphemeralAppsOnStartup,
202                 weak_ptr_factory_.GetWeakPtr()),
203      base::TimeDelta::FromSeconds(kDisableAppsOnStartupDelay));
204}
205
206void EphemeralAppService::InitEphemeralAppCount() {
207  scoped_ptr<ExtensionSet> extensions =
208      ExtensionRegistry::Get(profile_)->GenerateInstalledExtensionsSet();
209  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
210  DCHECK(prefs);
211
212  ephemeral_app_count_ = 0;
213  for (ExtensionSet::const_iterator it = extensions->begin();
214       it != extensions->end(); ++it) {
215    const Extension* extension = it->get();
216    if (prefs->IsEphemeralApp(extension->id()))
217      ++ephemeral_app_count_;
218  }
219}
220
221void EphemeralAppService::DisableEphemeralApp(const std::string& app_id) {
222  if (!extensions::util::IsEphemeralApp(app_id, profile_) ||
223      !extensions::util::IsExtensionIdle(app_id, profile_)) {
224    return;
225  }
226
227  // After an ephemeral app has stopped running, unload it from extension
228  // system and disable it to prevent all background activity.
229  ExtensionService* service =
230      ExtensionSystem::Get(profile_)->extension_service();
231  DCHECK(service);
232  service->DisableExtension(app_id, Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
233}
234
235void EphemeralAppService::DisableEphemeralAppsOnStartup() {
236  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
237  DCHECK(prefs);
238  ExtensionService* service =
239      ExtensionSystem::Get(profile_)->extension_service();
240  DCHECK(service);
241
242  // Ensure that all inactive ephemeral apps are disabled to prevent all
243  // background activity. This is done on startup to catch any apps that escaped
244  // being disabled on shutdown.
245  scoped_ptr<ExtensionSet> extensions =
246      ExtensionRegistry::Get(profile_)->GenerateInstalledExtensionsSet();
247  for (ExtensionSet::const_iterator it = extensions->begin();
248       it != extensions->end();
249       ++it) {
250    const Extension* extension = it->get();
251    if (!prefs->IsEphemeralApp(extension->id()))
252      continue;
253
254    // Only V2 apps are installed ephemerally. Remove other ephemeral app types
255    // that were cached before this policy was introduced.
256    if (!extension->is_platform_app()) {
257      service->UninstallExtension(
258          extension->id(),
259          extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
260          base::Bind(&base::DoNothing),
261          NULL);
262      continue;
263    }
264
265    if (!prefs->HasDisableReason(extension->id(),
266                                 Extension::DISABLE_INACTIVE_EPHEMERAL_APP) &&
267        !prefs->IsExtensionRunning(extension->id()) &&
268        extensions::util::IsExtensionIdle(extension->id(), profile_)) {
269      service->DisableExtension(extension->id(),
270                                Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
271    }
272  }
273}
274
275void EphemeralAppService::HandleEphemeralAppPromoted(const Extension* app) {
276  // When ephemeral apps are promoted to regular install apps, remove the
277  // DISABLE_INACTIVE_EPHEMERAL_APP reason and enable the app if there are no
278  // other reasons.
279  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
280  DCHECK(prefs);
281
282  int disable_reasons = prefs->GetDisableReasons(app->id());
283  if (disable_reasons & Extension::DISABLE_INACTIVE_EPHEMERAL_APP) {
284    prefs->RemoveDisableReason(app->id(),
285                               Extension::DISABLE_INACTIVE_EPHEMERAL_APP);
286    if (disable_reasons == Extension::DISABLE_INACTIVE_EPHEMERAL_APP)
287      prefs->SetExtensionState(app->id(), Extension::ENABLED);
288  }
289}
290
291void EphemeralAppService::TriggerGarbageCollect(const base::TimeDelta& delay) {
292  if (!garbage_collect_apps_timer_.IsRunning()) {
293    garbage_collect_apps_timer_.Start(
294        FROM_HERE,
295        delay,
296        this,
297        &EphemeralAppService::GarbageCollectApps);
298  }
299}
300
301void EphemeralAppService::GarbageCollectApps() {
302  ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
303  DCHECK(registry);
304  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile_);
305  DCHECK(prefs);
306
307  scoped_ptr<ExtensionSet> extensions =
308      registry->GenerateInstalledExtensionsSet();
309
310  int app_count = 0;
311  LaunchTimeAppMap app_launch_times;
312  std::set<std::string> remove_app_ids;
313
314  // Populate a list of ephemeral apps, ordered by their last launch time.
315  for (ExtensionSet::const_iterator it = extensions->begin();
316       it != extensions->end(); ++it) {
317    const Extension* extension = it->get();
318    if (!prefs->IsEphemeralApp(extension->id()))
319      continue;
320
321    ++app_count;
322
323    // Do not remove ephemeral apps that are running.
324    if (!extensions::util::IsExtensionIdle(extension->id(), profile_))
325      continue;
326
327    base::Time last_launch_time = prefs->GetLastLaunchTime(extension->id());
328
329    // If the last launch time is invalid, this may be because it was just
330    // installed. So use the install time. If this is also null for some reason,
331    // the app will be removed.
332    if (last_launch_time.is_null())
333      last_launch_time = prefs->GetInstallTime(extension->id());
334
335    app_launch_times.insert(std::make_pair(last_launch_time, extension->id()));
336  }
337
338  ExtensionService* service =
339      ExtensionSystem::Get(profile_)->extension_service();
340  DCHECK(service);
341  // Execute the eviction policies and remove apps marked for deletion.
342  if (!app_launch_times.empty()) {
343    GetAppsToRemove(app_count, app_launch_times, &remove_app_ids);
344
345    for (std::set<std::string>::const_iterator id = remove_app_ids.begin();
346         id != remove_app_ids.end(); ++id) {
347      // Protect against cascading uninstalls.
348      if (!registry->GetExtensionById(*id, ExtensionRegistry::EVERYTHING))
349        continue;
350
351      service->UninstallExtension(
352          *id,
353          extensions::UNINSTALL_REASON_ORPHANED_EPHEMERAL_EXTENSION,
354          base::Bind(&base::DoNothing),
355          NULL);
356    }
357  }
358}
359
360// static
361void EphemeralAppService::GetAppsToRemove(
362    int app_count,
363    const LaunchTimeAppMap& app_launch_times,
364    std::set<std::string>* remove_app_ids) {
365  base::Time time_now = base::Time::Now();
366  const base::Time inactive_threshold =
367      time_now - base::TimeDelta::FromDays(kAppInactiveThreshold);
368  const base::Time keep_threshold =
369      time_now - base::TimeDelta::FromDays(kAppKeepThreshold);
370
371  // Visit the apps in order of least recently to most recently launched.
372  for (LaunchTimeAppMap::const_iterator it = app_launch_times.begin();
373       it != app_launch_times.end(); ++it) {
374    // Cannot remove apps that have been launched recently. So break when we
375    // reach the new apps.
376    if (it->first > keep_threshold)
377        break;
378
379    // Remove ephemeral apps that have been inactive for a while or if the cache
380    // is larger than the desired size.
381    if (it->first < inactive_threshold || app_count > kMaxEphemeralAppsCount) {
382      remove_app_ids->insert(it->second);
383      --app_count;
384    } else {
385      break;
386    }
387  }
388}
389