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