1 // Copyright (c) 2011 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 <string>
6
7 #include "base/base_paths.h"
8 #include "base/command_line.h"
9 #include "base/logging.h"
10 #include "base/utf_string_conversions.h"
11 #include "chrome/app/chrome_command_ids.h"
12 #include "chrome/browser/background_application_list_model.h"
13 #include "chrome/browser/background_mode_manager.h"
14 #include "chrome/browser/extensions/extension_service.h"
15 #include "chrome/browser/metrics/user_metrics.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/status_icons/status_icon.h"
18 #include "chrome/browser/status_icons/status_tray.h"
19 #include "chrome/browser/ui/browser_list.h"
20 #include "chrome/common/chrome_switches.h"
21 #include "chrome/common/extensions/extension.h"
22 #include "chrome/common/pref_names.h"
23 #include "content/common/notification_service.h"
24 #include "content/common/notification_type.h"
25 #include "grit/chromium_strings.h"
26 #include "grit/generated_resources.h"
27 #include "grit/theme_resources.h"
28 #include "ui/base/l10n/l10n_util.h"
29 #include "ui/base/resource/resource_bundle.h"
30
OnApplicationDataChanged(const Extension * extension)31 void BackgroundModeManager::OnApplicationDataChanged(
32 const Extension* extension) {
33 UpdateContextMenuEntryIcon(extension);
34 }
35
OnApplicationListChanged()36 void BackgroundModeManager::OnApplicationListChanged() {
37 UpdateStatusTrayIconContextMenu();
38 }
39
BackgroundModeManager(Profile * profile,CommandLine * command_line)40 BackgroundModeManager::BackgroundModeManager(Profile* profile,
41 CommandLine* command_line)
42 : profile_(profile),
43 applications_(profile),
44 background_app_count_(0),
45 context_menu_(NULL),
46 context_menu_application_offset_(0),
47 in_background_mode_(false),
48 keep_alive_for_startup_(false),
49 status_tray_(NULL),
50 status_icon_(NULL) {
51 // If background mode is disabled, just exit - don't listen for any
52 // notifications.
53 if (!IsBackgroundModeEnabled(command_line))
54 return;
55
56 // Keep the browser alive until extensions are done loading - this is needed
57 // by the --no-startup-window flag. We want to stay alive until we load
58 // extensions, at which point we should either run in background mode (if
59 // there are background apps) or exit if there are none.
60 if (command_line->HasSwitch(switches::kNoStartupWindow)) {
61 keep_alive_for_startup_ = true;
62 BrowserList::StartKeepAlive();
63 }
64
65 // If the -keep-alive-for-test flag is passed, then always keep chrome running
66 // in the background until the user explicitly terminates it, by acting as if
67 // we loaded a background app.
68 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kKeepAliveForTest))
69 OnBackgroundAppLoaded();
70
71 // Listen for when extensions are loaded/unloaded so we can track the
72 // number of background apps and modify our keep-alive and launch-on-startup
73 // state appropriately.
74 registrar_.Add(this, NotificationType::EXTENSION_LOADED,
75 Source<Profile>(profile));
76 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
77 Source<Profile>(profile));
78
79 // Check for the presence of background apps after all extensions have been
80 // loaded, to handle the case where an extension has been manually removed
81 // while Chrome was not running.
82 registrar_.Add(this, NotificationType::EXTENSIONS_READY,
83 Source<Profile>(profile));
84
85 // Listen for the application shutting down so we can decrement our KeepAlive
86 // count.
87 registrar_.Add(this, NotificationType::APP_TERMINATING,
88 NotificationService::AllSources());
89
90 applications_.AddObserver(this);
91 }
92
~BackgroundModeManager()93 BackgroundModeManager::~BackgroundModeManager() {
94 applications_.RemoveObserver(this);
95
96 // We're going away, so exit background mode (does nothing if we aren't in
97 // background mode currently). This is primarily needed for unit tests,
98 // because in an actual running system we'd get an APP_TERMINATING
99 // notification before being destroyed.
100 EndBackgroundMode();
101 }
102
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)103 void BackgroundModeManager::Observe(NotificationType type,
104 const NotificationSource& source,
105 const NotificationDetails& details) {
106 switch (type.value) {
107 case NotificationType::EXTENSIONS_READY:
108 // Extensions are loaded, so we don't need to manually keep the browser
109 // process alive any more when running in no-startup-window mode.
110 EndKeepAliveForStartup();
111
112 // On a Mac, we use 'login items' mechanism which has user-facing UI so we
113 // don't want to stomp on user choice every time we start and load
114 // registered extensions. This means that if a background app is removed
115 // or added while Chrome is not running, we could leave Chrome in the
116 // wrong state, but this is better than constantly forcing Chrome to
117 // launch on startup even after the user removes the LoginItem manually.
118 #if !defined(OS_MACOSX)
119 EnableLaunchOnStartup(background_app_count_ > 0);
120 #endif
121 break;
122 case NotificationType::EXTENSION_LOADED: {
123 Extension* extension = Details<Extension>(details).ptr();
124 if (BackgroundApplicationListModel::IsBackgroundApp(*extension)) {
125 // Extensions loaded after the ExtensionsService is ready should be
126 // treated as new installs.
127 if (profile_->GetExtensionService()->is_ready())
128 OnBackgroundAppInstalled(extension);
129 OnBackgroundAppLoaded();
130 }
131 }
132 break;
133 case NotificationType::EXTENSION_UNLOADED:
134 if (BackgroundApplicationListModel::IsBackgroundApp(
135 *Details<UnloadedExtensionInfo>(details)->extension)) {
136 Details<UnloadedExtensionInfo> info =
137 Details<UnloadedExtensionInfo>(details);
138 // If we already got an unload notification when it was disabled, ignore
139 // this one.
140 // TODO(atwilson): Change BackgroundModeManager to use
141 // BackgroundApplicationListModel instead of tracking the count here.
142 if (info->already_disabled)
143 return;
144 OnBackgroundAppUnloaded();
145 OnBackgroundAppUninstalled();
146 }
147 break;
148 case NotificationType::APP_TERMINATING:
149 // Make sure we aren't still keeping the app alive (only happens if we
150 // don't receive an EXTENSIONS_READY notification for some reason).
151 EndKeepAliveForStartup();
152 // Performing an explicit shutdown, so exit background mode (does nothing
153 // if we aren't in background mode currently).
154 EndBackgroundMode();
155 // Shutting down, so don't listen for any more notifications so we don't
156 // try to re-enter/exit background mode again.
157 registrar_.RemoveAll();
158 break;
159 default:
160 NOTREACHED();
161 break;
162 }
163 }
164
EndKeepAliveForStartup()165 void BackgroundModeManager::EndKeepAliveForStartup() {
166 if (keep_alive_for_startup_) {
167 keep_alive_for_startup_ = false;
168 // We call this via the message queue to make sure we don't try to end
169 // keep-alive (which can shutdown Chrome) before the message loop has
170 // started.
171 MessageLoop::current()->PostTask(
172 FROM_HERE, NewRunnableFunction(BrowserList::EndKeepAlive));
173 }
174 }
175
OnBackgroundAppLoaded()176 void BackgroundModeManager::OnBackgroundAppLoaded() {
177 // When a background app loads, increment our count and also enable
178 // KeepAlive mode if the preference is set.
179 background_app_count_++;
180 if (background_app_count_ == 1)
181 StartBackgroundMode();
182 }
183
StartBackgroundMode()184 void BackgroundModeManager::StartBackgroundMode() {
185 // Don't bother putting ourselves in background mode if we're already there.
186 if (in_background_mode_)
187 return;
188
189 // Mark ourselves as running in background mode.
190 in_background_mode_ = true;
191
192 // Put ourselves in KeepAlive mode and create a status tray icon.
193 BrowserList::StartKeepAlive();
194
195 // Display a status icon to exit Chrome.
196 CreateStatusTrayIcon();
197 }
198
OnBackgroundAppUnloaded()199 void BackgroundModeManager::OnBackgroundAppUnloaded() {
200 // When a background app unloads, decrement our count and also end
201 // KeepAlive mode if appropriate.
202 background_app_count_--;
203 DCHECK(background_app_count_ >= 0);
204 if (background_app_count_ == 0)
205 EndBackgroundMode();
206 }
207
EndBackgroundMode()208 void BackgroundModeManager::EndBackgroundMode() {
209 if (!in_background_mode_)
210 return;
211 in_background_mode_ = false;
212
213 // End KeepAlive mode and blow away our status tray icon.
214 BrowserList::EndKeepAlive();
215 RemoveStatusTrayIcon();
216 }
217
OnBackgroundAppInstalled(const Extension * extension)218 void BackgroundModeManager::OnBackgroundAppInstalled(
219 const Extension* extension) {
220 // We're installing a background app. If this is the first background app
221 // being installed, make sure we are set to launch on startup.
222 if (background_app_count_ == 0)
223 EnableLaunchOnStartup(true);
224
225 // Notify the user that a background app has been installed.
226 if (extension) // NULL when called by unit tests.
227 DisplayAppInstalledNotification(extension);
228 }
229
OnBackgroundAppUninstalled()230 void BackgroundModeManager::OnBackgroundAppUninstalled() {
231 // When uninstalling a background app, disable launch on startup if
232 // we have no more background apps.
233 if (background_app_count_ == 0)
234 EnableLaunchOnStartup(false);
235 }
236
CreateStatusTrayIcon()237 void BackgroundModeManager::CreateStatusTrayIcon() {
238 // Only need status icons on windows/linux. ChromeOS doesn't allow exiting
239 // Chrome and Mac can use the dock icon instead.
240 #if !defined(OS_MACOSX) && !defined(OS_CHROMEOS)
241 if (!status_tray_)
242 status_tray_ = profile_->GetStatusTray();
243 #endif
244
245 // If the platform doesn't support status icons, or we've already created
246 // our status icon, just return.
247 if (!status_tray_ || status_icon_)
248 return;
249 status_icon_ = status_tray_->CreateStatusIcon();
250 if (!status_icon_)
251 return;
252
253 // Set the image and add ourselves as a click observer on it
254 SkBitmap* bitmap = ResourceBundle::GetSharedInstance().GetBitmapNamed(
255 IDR_STATUS_TRAY_ICON);
256 status_icon_->SetImage(*bitmap);
257 status_icon_->SetToolTip(l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
258 UpdateStatusTrayIconContextMenu();
259 }
260
UpdateContextMenuEntryIcon(const Extension * extension)261 void BackgroundModeManager::UpdateContextMenuEntryIcon(
262 const Extension* extension) {
263 if (!context_menu_)
264 return;
265 context_menu_->SetIcon(
266 context_menu_application_offset_ + applications_.GetPosition(extension),
267 *(applications_.GetIcon(extension)));
268 status_icon_->SetContextMenu(context_menu_); // for Update effect
269 }
270
UpdateStatusTrayIconContextMenu()271 void BackgroundModeManager::UpdateStatusTrayIconContextMenu() {
272 if (!status_icon_)
273 return;
274
275 // Create a context menu item for Chrome.
276 ui::SimpleMenuModel* menu = new ui::SimpleMenuModel(this);
277 // Add About item
278 menu->AddItem(IDC_ABOUT, l10n_util::GetStringFUTF16(IDS_ABOUT,
279 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)));
280 menu->AddItem(IDC_OPTIONS, GetPreferencesMenuLabel());
281 menu->AddItemWithStringId(IDC_TASK_MANAGER, IDS_TASK_MANAGER);
282 menu->AddSeparator();
283 int position = 0;
284 context_menu_application_offset_ = menu->GetItemCount();
285 for (ExtensionList::const_iterator cursor = applications_.begin();
286 cursor != applications_.end();
287 ++cursor, ++position) {
288 const SkBitmap* icon = applications_.GetIcon(*cursor);
289 DCHECK(position == applications_.GetPosition(*cursor));
290 const std::string& name = (*cursor)->name();
291 menu->AddItem(position, UTF8ToUTF16(name));
292 if (icon)
293 menu->SetIcon(menu->GetItemCount() - 1, *icon);
294 }
295 if (applications_.size() > 0)
296 menu->AddSeparator();
297 menu->AddItemWithStringId(IDC_EXIT, IDS_EXIT);
298 context_menu_ = menu;
299 status_icon_->SetContextMenu(menu);
300 }
301
IsCommandIdChecked(int command_id) const302 bool BackgroundModeManager::IsCommandIdChecked(int command_id) const {
303 return false;
304 }
305
IsCommandIdEnabled(int command_id) const306 bool BackgroundModeManager::IsCommandIdEnabled(int command_id) const {
307 // For now, we do not support disabled items.
308 return true;
309 }
310
GetAcceleratorForCommandId(int command_id,ui::Accelerator * accelerator)311 bool BackgroundModeManager::GetAcceleratorForCommandId(
312 int command_id,
313 ui::Accelerator* accelerator) {
314 // No accelerators for status icon context menus.
315 return false;
316 }
317
RemoveStatusTrayIcon()318 void BackgroundModeManager::RemoveStatusTrayIcon() {
319 if (status_icon_)
320 status_tray_->RemoveStatusIcon(status_icon_);
321 status_icon_ = NULL;
322 context_menu_ = NULL; // Do not delete, points within status_icon_
323 }
324
ExecuteApplication(int item)325 void BackgroundModeManager::ExecuteApplication(int item) {
326 DCHECK(item >= 0 && item < static_cast<int>(applications_.size()));
327 Browser* browser = BrowserList::GetLastActive();
328 if (!browser) {
329 Browser::OpenEmptyWindow(profile_);
330 browser = BrowserList::GetLastActive();
331 }
332 const Extension* extension = applications_.GetExtension(item);
333 browser->OpenApplicationTab(profile_, extension, NULL);
334 }
335
ExecuteCommand(int item)336 void BackgroundModeManager::ExecuteCommand(int item) {
337 switch (item) {
338 case IDC_ABOUT:
339 GetBrowserWindow()->OpenAboutChromeDialog();
340 break;
341 case IDC_EXIT:
342 UserMetrics::RecordAction(UserMetricsAction("Exit"), profile_);
343 BrowserList::CloseAllBrowsersAndExit();
344 break;
345 case IDC_OPTIONS:
346 GetBrowserWindow()->OpenOptionsDialog();
347 break;
348 case IDC_TASK_MANAGER:
349 GetBrowserWindow()->OpenTaskManager(true);
350 break;
351 default:
352 ExecuteApplication(item);
353 break;
354 }
355 }
356
GetBrowserWindow()357 Browser* BackgroundModeManager::GetBrowserWindow() {
358 Browser* browser = BrowserList::GetLastActive();
359 if (!browser) {
360 Browser::OpenEmptyWindow(profile_);
361 browser = BrowserList::GetLastActive();
362 }
363 return browser;
364 }
365
366 // static
IsBackgroundModeEnabled(const CommandLine * command_line)367 bool BackgroundModeManager::IsBackgroundModeEnabled(
368 const CommandLine* command_line) {
369
370 // Background mode is disabled if the appropriate flag is passed, or if
371 // extensions are disabled. It's always disabled on chromeos since chrome
372 // is always running on that platform, making it superfluous.
373 #if defined(OS_CHROMEOS)
374 return false;
375 #else
376 bool background_mode_enabled =
377 !command_line->HasSwitch(switches::kDisableBackgroundMode) &&
378 !command_line->HasSwitch(switches::kDisableExtensions);
379 return background_mode_enabled;
380 #endif
381 }
382
383 // static
RegisterPrefs(PrefService * prefs)384 void BackgroundModeManager::RegisterPrefs(PrefService* prefs) {
385 prefs->RegisterBooleanPref(prefs::kUserCreatedLoginItem, false);
386 }
387