1 // Copyright (c) 2012 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/ui/extensions/application_launch.h" 6 7 #include <string> 8 9 #include "apps/launcher.h" 10 #include "base/command_line.h" 11 #include "base/metrics/histogram.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "chrome/browser/app_mode/app_mode_utils.h" 14 #include "chrome/browser/apps/per_app_settings_service.h" 15 #include "chrome/browser/apps/per_app_settings_service_factory.h" 16 #include "chrome/browser/extensions/extension_service.h" 17 #include "chrome/browser/extensions/launch_util.h" 18 #include "chrome/browser/extensions/tab_helper.h" 19 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/ui/app_list/app_list_service.h" 21 #include "chrome/browser/ui/browser.h" 22 #include "chrome/browser/ui/browser_commands.h" 23 #include "chrome/browser/ui/browser_finder.h" 24 #include "chrome/browser/ui/browser_tabstrip.h" 25 #include "chrome/browser/ui/browser_window.h" 26 #include "chrome/browser/ui/extensions/extension_enable_flow.h" 27 #include "chrome/browser/ui/extensions/extension_enable_flow_delegate.h" 28 #include "chrome/browser/ui/tabs/tab_strip_model.h" 29 #include "chrome/browser/web_applications/web_app.h" 30 #include "chrome/common/chrome_switches.h" 31 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" 32 #include "chrome/common/extensions/manifest_url_handler.h" 33 #include "chrome/common/url_constants.h" 34 #include "content/public/browser/render_view_host.h" 35 #include "content/public/browser/web_contents.h" 36 #include "content/public/common/renderer_preferences.h" 37 #include "extensions/browser/extension_prefs.h" 38 #include "extensions/browser/extension_registry.h" 39 #include "extensions/browser/extension_system.h" 40 #include "extensions/common/constants.h" 41 #include "extensions/common/extension.h" 42 #include "extensions/common/features/feature.h" 43 #include "extensions/common/features/feature_provider.h" 44 #include "grit/generated_resources.h" 45 #include "ui/base/window_open_disposition.h" 46 #include "ui/gfx/rect.h" 47 48 #if defined(OS_MACOSX) 49 #include "chrome/browser/ui/browser_commands_mac.h" 50 #endif 51 52 using content::WebContents; 53 using extensions::Extension; 54 using extensions::ExtensionPrefs; 55 using extensions::ExtensionRegistry; 56 57 namespace { 58 59 // Attempts to launch a packaged app, prompting the user to enable it if 60 // necessary. If a prompt is required it will be shown inside the AppList. 61 // This class manages its own lifetime. 62 class EnableViaAppListFlow : public ExtensionEnableFlowDelegate { 63 public: 64 EnableViaAppListFlow(ExtensionService* service, 65 Profile* profile, 66 chrome::HostDesktopType desktop_type, 67 const std::string& extension_id, 68 const base::Closure& callback) 69 : service_(service), 70 profile_(profile), 71 desktop_type_(desktop_type), 72 extension_id_(extension_id), 73 callback_(callback) { 74 } 75 76 virtual ~EnableViaAppListFlow() { 77 } 78 79 void Run() { 80 DCHECK(!service_->IsExtensionEnabled(extension_id_)); 81 flow_.reset(new ExtensionEnableFlow(profile_, extension_id_, this)); 82 flow_->StartForCurrentlyNonexistentWindow( 83 base::Bind(&EnableViaAppListFlow::ShowAppList, base::Unretained(this))); 84 } 85 86 private: 87 gfx::NativeWindow ShowAppList() { 88 AppListService* app_list_service = AppListService::Get(desktop_type_); 89 app_list_service->Show(); 90 return app_list_service->GetAppListWindow(); 91 } 92 93 // ExtensionEnableFlowDelegate overrides. 94 virtual void ExtensionEnableFlowFinished() OVERRIDE { 95 const Extension* extension = 96 service_->GetExtensionById(extension_id_, false); 97 if (!extension) 98 return; 99 callback_.Run(); 100 delete this; 101 } 102 103 virtual void ExtensionEnableFlowAborted(bool user_initiated) OVERRIDE { 104 delete this; 105 } 106 107 ExtensionService* service_; 108 Profile* profile_; 109 chrome::HostDesktopType desktop_type_; 110 std::string extension_id_; 111 base::Closure callback_; 112 scoped_ptr<ExtensionEnableFlow> flow_; 113 114 DISALLOW_COPY_AND_ASSIGN(EnableViaAppListFlow); 115 }; 116 117 const Extension* GetExtension(const AppLaunchParams& params) { 118 if (params.extension_id.empty()) 119 return NULL; 120 ExtensionRegistry* registry = ExtensionRegistry::Get(params.profile); 121 return registry->GetExtensionById(params.extension_id, 122 ExtensionRegistry::ENABLED | 123 ExtensionRegistry::DISABLED | 124 ExtensionRegistry::TERMINATED); 125 } 126 127 // Get the launch URL for a given extension, with optional override/fallback. 128 // |override_url|, if non-empty, will be preferred over the extension's 129 // launch url. 130 GURL UrlForExtension(const Extension* extension, 131 const GURL& override_url) { 132 if (!extension) 133 return override_url; 134 135 GURL url; 136 if (!override_url.is_empty()) { 137 DCHECK(extension->web_extent().MatchesURL(override_url) || 138 override_url.GetOrigin() == extension->url()); 139 url = override_url; 140 } else { 141 url = extensions::AppLaunchInfo::GetFullLaunchURL(extension); 142 } 143 144 // For extensions lacking launch urls, determine a reasonable fallback. 145 if (!url.is_valid()) { 146 url = extensions::ManifestURL::GetOptionsPage(extension); 147 if (!url.is_valid()) 148 url = GURL(chrome::kChromeUIExtensionsURL); 149 } 150 151 return url; 152 } 153 154 ui::WindowShowState DetermineWindowShowState( 155 Profile* profile, 156 extensions::LaunchContainer container, 157 const Extension* extension) { 158 if (!extension || container != extensions::LAUNCH_CONTAINER_WINDOW) 159 return ui::SHOW_STATE_DEFAULT; 160 161 if (chrome::IsRunningInForcedAppMode()) 162 return ui::SHOW_STATE_FULLSCREEN; 163 164 #if defined(USE_ASH) 165 // In ash, LAUNCH_TYPE_FULLSCREEN launches in a maximized app window and 166 // LAUNCH_TYPE_WINDOW launches in a normal app window. 167 extensions::LaunchType launch_type = 168 extensions::GetLaunchType(ExtensionPrefs::Get(profile), extension); 169 if (launch_type == extensions::LAUNCH_TYPE_FULLSCREEN) 170 return ui::SHOW_STATE_MAXIMIZED; 171 else if (launch_type == extensions::LAUNCH_TYPE_WINDOW) 172 return ui::SHOW_STATE_NORMAL; 173 #endif 174 175 return ui::SHOW_STATE_DEFAULT; 176 } 177 178 WebContents* OpenApplicationWindow(const AppLaunchParams& params) { 179 Profile* const profile = params.profile; 180 const Extension* const extension = GetExtension(params); 181 const GURL url_input = params.override_url; 182 183 DCHECK(!url_input.is_empty() || extension); 184 GURL url = UrlForExtension(extension, url_input); 185 std::string app_name = extension ? 186 web_app::GenerateApplicationNameFromExtensionId(extension->id()) : 187 web_app::GenerateApplicationNameFromURL(url); 188 189 gfx::Rect initial_bounds; 190 if (!params.override_bounds.IsEmpty()) { 191 initial_bounds = params.override_bounds; 192 } else if (extension) { 193 initial_bounds.set_width( 194 extensions::AppLaunchInfo::GetLaunchWidth(extension)); 195 initial_bounds.set_height( 196 extensions::AppLaunchInfo::GetLaunchHeight(extension)); 197 } 198 199 Browser::CreateParams browser_params( 200 Browser::CreateParams::CreateForApp(app_name, 201 true /* trusted_source */, 202 initial_bounds, 203 profile, 204 params.desktop_type)); 205 206 browser_params.initial_show_state = DetermineWindowShowState(profile, 207 params.container, 208 extension); 209 210 Browser* browser = new Browser(browser_params); 211 212 WebContents* web_contents = chrome::AddSelectedTabWithURL( 213 browser, url, content::PAGE_TRANSITION_AUTO_TOPLEVEL); 214 web_contents->GetMutableRendererPrefs()->can_accept_load_drops = false; 215 web_contents->GetRenderViewHost()->SyncRendererPrefs(); 216 217 browser->window()->Show(); 218 219 // TODO(jcampan): http://crbug.com/8123 we should not need to set the initial 220 // focus explicitly. 221 web_contents->SetInitialFocus(); 222 return web_contents; 223 } 224 225 WebContents* OpenApplicationTab(const AppLaunchParams& launch_params) { 226 const Extension* extension = GetExtension(launch_params); 227 CHECK(extension); 228 Profile* const profile = launch_params.profile; 229 WindowOpenDisposition disposition = launch_params.disposition; 230 231 Browser* browser = chrome::FindTabbedBrowser(profile, 232 false, 233 launch_params.desktop_type); 234 WebContents* contents = NULL; 235 if (!browser) { 236 // No browser for this profile, need to open a new one. 237 browser = new Browser(Browser::CreateParams(Browser::TYPE_TABBED, 238 profile, 239 launch_params.desktop_type)); 240 browser->window()->Show(); 241 // There's no current tab in this browser window, so add a new one. 242 disposition = NEW_FOREGROUND_TAB; 243 } else { 244 // For existing browser, ensure its window is shown and activated. 245 browser->window()->Show(); 246 browser->window()->Activate(); 247 } 248 249 extensions::LaunchType launch_type = 250 extensions::GetLaunchType(ExtensionPrefs::Get(profile), extension); 251 UMA_HISTOGRAM_ENUMERATION("Extensions.AppTabLaunchType", launch_type, 100); 252 253 int add_type = TabStripModel::ADD_ACTIVE; 254 if (launch_type == extensions::LAUNCH_TYPE_PINNED) 255 add_type |= TabStripModel::ADD_PINNED; 256 257 GURL extension_url = UrlForExtension(extension, launch_params.override_url); 258 chrome::NavigateParams params(browser, extension_url, 259 content::PAGE_TRANSITION_AUTO_TOPLEVEL); 260 params.tabstrip_add_types = add_type; 261 params.disposition = disposition; 262 263 if (disposition == CURRENT_TAB) { 264 WebContents* existing_tab = 265 browser->tab_strip_model()->GetActiveWebContents(); 266 TabStripModel* model = browser->tab_strip_model(); 267 int tab_index = model->GetIndexOfWebContents(existing_tab); 268 269 existing_tab->OpenURL(content::OpenURLParams( 270 extension_url, 271 content::Referrer(existing_tab->GetURL(), 272 blink::WebReferrerPolicyDefault), 273 disposition, content::PAGE_TRANSITION_LINK, false)); 274 // Reset existing_tab as OpenURL() may have clobbered it. 275 existing_tab = browser->tab_strip_model()->GetActiveWebContents(); 276 if (params.tabstrip_add_types & TabStripModel::ADD_PINNED) { 277 model->SetTabPinned(tab_index, true); 278 // Pinning may have moved the tab. 279 tab_index = model->GetIndexOfWebContents(existing_tab); 280 } 281 if (params.tabstrip_add_types & TabStripModel::ADD_ACTIVE) 282 model->ActivateTabAt(tab_index, true); 283 284 contents = existing_tab; 285 } else { 286 chrome::Navigate(¶ms); 287 contents = params.target_contents; 288 } 289 290 // On Chrome OS the host desktop type for a browser window is always set to 291 // HOST_DESKTOP_TYPE_ASH. On Windows 8 it is only the case for Chrome ASH 292 // in metro mode. 293 if (browser->host_desktop_type() == chrome::HOST_DESKTOP_TYPE_ASH) { 294 // In ash, LAUNCH_FULLSCREEN launches in the OpenApplicationWindow function 295 // i.e. it should not reach here. 296 DCHECK(launch_type != extensions::LAUNCH_TYPE_FULLSCREEN); 297 } else { 298 // TODO(skerner): If we are already in full screen mode, and the user 299 // set the app to open as a regular or pinned tab, what should happen? 300 // Today we open the tab, but stay in full screen mode. Should we leave 301 // full screen mode in this case? 302 if (launch_type == extensions::LAUNCH_TYPE_FULLSCREEN && 303 !browser->window()->IsFullscreen()) { 304 #if defined(OS_MACOSX) 305 chrome::ToggleFullscreenWithChromeOrFallback(browser); 306 #else 307 chrome::ToggleFullscreenMode(browser); 308 #endif 309 } 310 } 311 return contents; 312 } 313 314 WebContents* OpenEnabledApplication(const AppLaunchParams& params) { 315 const Extension* extension = GetExtension(params); 316 if (!extension) 317 return NULL; 318 Profile* profile = params.profile; 319 320 WebContents* tab = NULL; 321 ExtensionPrefs* prefs = ExtensionPrefs::Get(profile); 322 prefs->SetActiveBit(extension->id(), true); 323 324 UMA_HISTOGRAM_ENUMERATION( 325 "Extensions.AppLaunchContainer", params.container, 100); 326 327 if (CanLaunchViaEvent(extension)) { 328 // Remember what desktop the launch happened on so that when the app opens a 329 // window we can open them on the right desktop. 330 PerAppSettingsServiceFactory::GetForBrowserContext(profile)-> 331 SetDesktopLastLaunchedFrom(extension->id(), params.desktop_type); 332 333 apps::LaunchPlatformAppWithCommandLine( 334 profile, extension, params.command_line, params.current_directory); 335 return NULL; 336 } 337 338 // Record v1 app launch. Platform app launch is recorded when dispatching 339 // the onLaunched event. 340 prefs->SetLastLaunchTime(extension->id(), base::Time::Now()); 341 342 switch (params.container) { 343 case extensions::LAUNCH_CONTAINER_NONE: { 344 NOTREACHED(); 345 break; 346 } 347 case extensions::LAUNCH_CONTAINER_PANEL: 348 case extensions::LAUNCH_CONTAINER_WINDOW: 349 tab = OpenApplicationWindow(params); 350 break; 351 case extensions::LAUNCH_CONTAINER_TAB: { 352 tab = OpenApplicationTab(params); 353 break; 354 } 355 default: 356 NOTREACHED(); 357 break; 358 } 359 return tab; 360 } 361 362 } // namespace 363 364 AppLaunchParams::AppLaunchParams(Profile* profile, 365 const extensions::Extension* extension, 366 extensions::LaunchContainer container, 367 WindowOpenDisposition disposition) 368 : profile(profile), 369 extension_id(extension ? extension->id() : std::string()), 370 container(container), 371 disposition(disposition), 372 desktop_type(chrome::GetActiveDesktop()), 373 override_url(), 374 override_bounds(), 375 command_line(CommandLine::NO_PROGRAM) {} 376 377 AppLaunchParams::AppLaunchParams(Profile* profile, 378 const extensions::Extension* extension, 379 WindowOpenDisposition disposition) 380 : profile(profile), 381 extension_id(extension ? extension->id() : std::string()), 382 container(extensions::LAUNCH_CONTAINER_NONE), 383 disposition(disposition), 384 desktop_type(chrome::GetActiveDesktop()), 385 override_url(), 386 override_bounds(), 387 command_line(CommandLine::NO_PROGRAM) { 388 // Look up the app preference to find out the right launch container. Default 389 // is to launch as a regular tab. 390 container = 391 extensions::GetLaunchContainer(ExtensionPrefs::Get(profile), extension); 392 } 393 394 AppLaunchParams::AppLaunchParams(Profile* profile, 395 const extensions::Extension* extension, 396 int event_flags, 397 chrome::HostDesktopType desktop_type) 398 : profile(profile), 399 extension_id(extension ? extension->id() : std::string()), 400 container(extensions::LAUNCH_CONTAINER_NONE), 401 disposition(ui::DispositionFromEventFlags(event_flags)), 402 desktop_type(desktop_type), 403 override_url(), 404 override_bounds(), 405 command_line(CommandLine::NO_PROGRAM) { 406 if (disposition == NEW_FOREGROUND_TAB || disposition == NEW_BACKGROUND_TAB) { 407 container = extensions::LAUNCH_CONTAINER_TAB; 408 } else if (disposition == NEW_WINDOW) { 409 container = extensions::LAUNCH_CONTAINER_WINDOW; 410 } else { 411 // Look at preference to find the right launch container. If no preference 412 // is set, launch as a regular tab. 413 container = 414 extensions::GetLaunchContainer(ExtensionPrefs::Get(profile), extension); 415 disposition = NEW_FOREGROUND_TAB; 416 } 417 } 418 419 AppLaunchParams::~AppLaunchParams() { 420 } 421 422 WebContents* OpenApplication(const AppLaunchParams& params) { 423 return OpenEnabledApplication(params); 424 } 425 426 void OpenApplicationWithReenablePrompt(const AppLaunchParams& params) { 427 const Extension* extension = GetExtension(params); 428 if (!extension) 429 return; 430 Profile* profile = params.profile; 431 432 ExtensionService* service = 433 extensions::ExtensionSystem::Get(profile)->extension_service(); 434 if (!service->IsExtensionEnabled(extension->id()) || 435 extensions::ExtensionRegistry::Get(profile)->GetExtensionById( 436 extension->id(), extensions::ExtensionRegistry::TERMINATED)) { 437 (new EnableViaAppListFlow( 438 service, profile, params.desktop_type, extension->id(), 439 base::Bind(base::IgnoreResult(OpenEnabledApplication), params)))->Run(); 440 return; 441 } 442 443 OpenEnabledApplication(params); 444 } 445 446 WebContents* OpenAppShortcutWindow(Profile* profile, 447 const GURL& url) { 448 AppLaunchParams launch_params( 449 profile, 450 NULL, // this is a URL app. No extension. 451 extensions::LAUNCH_CONTAINER_WINDOW, 452 NEW_WINDOW); 453 launch_params.override_url = url; 454 455 WebContents* tab = OpenApplicationWindow(launch_params); 456 457 if (!tab) 458 return NULL; 459 460 // Set UPDATE_SHORTCUT as the pending web app action. This action is picked 461 // up in LoadingStateChanged to schedule a GetApplicationInfo. And when 462 // the web app info is available, extensions::TabHelper notifies Browser via 463 // OnDidGetApplicationInfo, which calls 464 // web_app::UpdateShortcutForTabContents when it sees UPDATE_SHORTCUT as 465 // pending web app action. 466 extensions::TabHelper::FromWebContents(tab)->set_pending_web_app_action( 467 extensions::TabHelper::UPDATE_SHORTCUT); 468 469 return tab; 470 } 471 472 bool CanLaunchViaEvent(const extensions::Extension* extension) { 473 const extensions::FeatureProvider* feature_provider = 474 extensions::FeatureProvider::GetAPIFeatures(); 475 extensions::Feature* feature = feature_provider->GetFeature("app.runtime"); 476 return feature->IsAvailableToExtension(extension).is_available(); 477 } 478