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 31 void BackgroundModeManager::OnApplicationDataChanged( 32 const Extension* extension) { 33 UpdateContextMenuEntryIcon(extension); 34 } 35 36 void BackgroundModeManager::OnApplicationListChanged() { 37 UpdateStatusTrayIconContextMenu(); 38 } 39 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 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 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 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 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 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 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 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 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 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 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 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 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 302 bool BackgroundModeManager::IsCommandIdChecked(int command_id) const { 303 return false; 304 } 305 306 bool BackgroundModeManager::IsCommandIdEnabled(int command_id) const { 307 // For now, we do not support disabled items. 308 return true; 309 } 310 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 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 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 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 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 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 384 void BackgroundModeManager::RegisterPrefs(PrefService* prefs) { 385 prefs->RegisterBooleanPref(prefs::kUserCreatedLoginItem, false); 386 } 387