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.
5 #include "apps/shell_window.h"
7 #include "apps/native_app_window.h"
8 #include "apps/shell_window_geometry_cache.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "base/values.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/extensions/extension_process_manager.h"
14 #include "chrome/browser/extensions/extension_system.h"
15 #include "chrome/browser/extensions/shell_window_registry.h"
16 #include "chrome/browser/extensions/suggest_permission_util.h"
17 #include "chrome/browser/lifetime/application_lifetime.h"
18 #include "chrome/browser/profiles/profile.h"
19 #include "chrome/common/extensions/extension.h"
20 #include "chrome/common/extensions/extension_constants.h"
21 #include "chrome/common/extensions/extension_messages.h"
22 #include "chrome/common/extensions/manifest_handlers/icons_handler.h"
23 #include "components/web_modal/web_contents_modal_dialog_manager.h"
24 #include "content/public/browser/invalidate_type.h"
25 #include "content/public/browser/navigation_entry.h"
26 #include "content/public/browser/notification_details.h"
27 #include "content/public/browser/notification_service.h"
28 #include "content/public/browser/notification_source.h"
29 #include "content/public/browser/notification_types.h"
30 #include "content/public/browser/render_view_host.h"
31 #include "content/public/browser/resource_dispatcher_host.h"
32 #include "content/public/browser/web_contents.h"
33 #include "content/public/common/media_stream_request.h"
34 #include "extensions/browser/view_type_utils.h"
35 #include "skia/ext/image_operations.h"
36 #include "third_party/skia/include/core/SkRegion.h"
37 #include "ui/gfx/image/image_skia.h"
38 #include "ui/gfx/screen.h"
40 using content::ConsoleMessageLevel
;
41 using content::WebContents
;
42 using extensions::APIPermission
;
43 using web_modal::WebContentsModalDialogHost
;
44 using web_modal::WebContentsModalDialogManager
;
47 const int kDefaultWidth
= 512;
48 const int kDefaultHeight
= 384;
54 ShellWindow::CreateParams::CreateParams()
55 : window_type(ShellWindow::WINDOW_TYPE_DEFAULT
),
56 frame(ShellWindow::FRAME_CHROME
),
57 transparent_background(false),
58 bounds(INT_MIN
, INT_MIN
, 0, 0),
59 creator_process_id(0),
60 state(ui::SHOW_STATE_DEFAULT
),
65 ShellWindow::CreateParams::~CreateParams() {}
67 ShellWindow::Delegate::~Delegate() {}
69 ShellWindow::ShellWindow(Profile
* profile
,
71 const extensions::Extension
* extension
)
73 extension_(extension
),
74 extension_id_(extension
->id()),
75 window_type_(WINDOW_TYPE_DEFAULT
),
77 image_loader_ptr_factory_(this),
78 fullscreen_for_window_api_(false),
79 fullscreen_for_tab_(false) {
82 void ShellWindow::Init(const GURL
& url
,
83 ShellWindowContents
* shell_window_contents
,
84 const CreateParams
& params
) {
85 // Initialize the render interface and web contents
86 shell_window_contents_
.reset(shell_window_contents
);
87 shell_window_contents_
->Initialize(profile(), url
);
88 WebContents
* web_contents
= shell_window_contents_
->GetWebContents();
89 delegate_
->InitWebContents(web_contents
);
90 WebContentsModalDialogManager::CreateForWebContents(web_contents
);
92 web_contents
->SetDelegate(this);
93 WebContentsModalDialogManager::FromWebContents(web_contents
)->
95 extensions::SetViewType(web_contents
, extensions::VIEW_TYPE_APP_SHELL
);
97 // Initialize the window
98 window_type_
= params
.window_type
;
100 gfx::Rect bounds
= params
.bounds
;
102 if (bounds
.width() == 0)
103 bounds
.set_width(kDefaultWidth
);
104 if (bounds
.height() == 0)
105 bounds
.set_height(kDefaultHeight
);
107 // If left and top are left undefined, the native shell window will center
108 // the window on the main screen in a platform-defined manner.
110 CreateParams new_params
= params
;
112 // Load cached state if it exists.
113 if (!params
.window_key
.empty()) {
114 window_key_
= params
.window_key
;
116 ShellWindowGeometryCache
* cache
= ShellWindowGeometryCache::Get(profile());
118 gfx::Rect cached_bounds
;
119 gfx::Rect cached_screen_bounds
;
120 ui::WindowShowState cached_state
= ui::SHOW_STATE_DEFAULT
;
121 if (cache
->GetGeometry(extension()->id(), params
.window_key
, &cached_bounds
,
122 &cached_screen_bounds
, &cached_state
)) {
123 // App window has cached screen bounds, make sure it fits on screen in
124 // case the screen resolution changed.
125 gfx::Screen
* screen
= gfx::Screen::GetNativeScreen();
126 gfx::Display display
= screen
->GetDisplayMatching(cached_bounds
);
127 gfx::Rect current_screen_bounds
= display
.work_area();
128 AdjustBoundsToBeVisibleOnScreen(cached_bounds
,
129 cached_screen_bounds
,
130 current_screen_bounds
,
133 new_params
.state
= cached_state
;
137 gfx::Size
& minimum_size
= new_params
.minimum_size
;
138 gfx::Size
& maximum_size
= new_params
.maximum_size
;
140 // In the case that minimum size > maximum size, we consider the minimum
141 // size to be more important.
142 if (maximum_size
.width() && maximum_size
.width() < minimum_size
.width())
143 maximum_size
.set_width(minimum_size
.width());
144 if (maximum_size
.height() && maximum_size
.height() < minimum_size
.height())
145 maximum_size
.set_height(minimum_size
.height());
147 if (maximum_size
.width() && bounds
.width() > maximum_size
.width())
148 bounds
.set_width(maximum_size
.width());
149 if (bounds
.width() != INT_MIN
&& bounds
.width() < minimum_size
.width())
150 bounds
.set_width(minimum_size
.width());
152 if (maximum_size
.height() && bounds
.height() > maximum_size
.height())
153 bounds
.set_height(maximum_size
.height());
154 if (bounds
.height() != INT_MIN
&& bounds
.height() < minimum_size
.height())
155 bounds
.set_height(minimum_size
.height());
157 new_params
.bounds
= bounds
;
159 native_app_window_
.reset(delegate_
->CreateNativeAppWindow(this, new_params
));
161 if (!new_params
.hidden
) {
162 if (window_type_is_panel())
163 GetBaseWindow()->ShowInactive(); // Panels are not activated by default.
165 GetBaseWindow()->Show();
168 if (new_params
.state
== ui::SHOW_STATE_FULLSCREEN
)
170 else if (new_params
.state
== ui::SHOW_STATE_MAXIMIZED
)
172 else if (new_params
.state
== ui::SHOW_STATE_MINIMIZED
)
175 OnNativeWindowChanged();
177 // When the render view host is changed, the native window needs to know
178 // about it in case it has any setup to do to make the renderer appear
179 // properly. In particular, on Windows, the view's clickthrough region needs
181 registrar_
.Add(this, content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED
,
182 content::Source
<content::NavigationController
>(
183 &web_contents
->GetController()));
184 registrar_
.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED
,
185 content::Source
<Profile
>(profile_
));
186 // Close when the browser process is exiting.
187 registrar_
.Add(this, chrome::NOTIFICATION_APP_TERMINATING
,
188 content::NotificationService::AllSources());
190 shell_window_contents_
->LoadContents(params
.creator_process_id
);
192 // Prevent the browser process from shutting down while this window is open.
193 chrome::StartKeepAlive();
195 UpdateExtensionAppIcon();
197 extensions::ShellWindowRegistry::Get(profile_
)->AddShellWindow(this);
200 ShellWindow::~ShellWindow() {
201 // Unregister now to prevent getting NOTIFICATION_APP_TERMINATING if we're the
203 registrar_
.RemoveAll();
205 // Remove shutdown prevention.
206 chrome::EndKeepAlive();
209 void ShellWindow::RequestMediaAccessPermission(
210 content::WebContents
* web_contents
,
211 const content::MediaStreamRequest
& request
,
212 const content::MediaResponseCallback
& callback
) {
213 delegate_
->RequestMediaAccessPermission(web_contents
, request
, callback
,
217 WebContents
* ShellWindow::OpenURLFromTab(WebContents
* source
,
218 const content::OpenURLParams
& params
) {
219 // Don't allow the current tab to be navigated. It would be nice to map all
220 // anchor tags (even those without target="_blank") to new tabs, but right
221 // now we can't distinguish between those and <meta> refreshes or window.href
222 // navigations, which we don't want to allow.
223 // TOOD(mihaip): Can we check for user gestures instead?
224 WindowOpenDisposition disposition
= params
.disposition
;
225 if (disposition
== CURRENT_TAB
) {
226 AddMessageToDevToolsConsole(
227 content::CONSOLE_MESSAGE_LEVEL_ERROR
,
229 "Can't open same-window link to \"%s\"; try target=\"_blank\".",
230 params
.url
.spec().c_str()));
234 // These dispositions aren't really navigations.
235 if (disposition
== SUPPRESS_OPEN
|| disposition
== SAVE_TO_DISK
||
236 disposition
== IGNORE_ACTION
) {
240 WebContents
* contents
= delegate_
->OpenURLFromTab(profile_
, source
,
243 AddMessageToDevToolsConsole(
244 content::CONSOLE_MESSAGE_LEVEL_ERROR
,
246 "Can't navigate to \"%s\"; apps do not support navigation.",
247 params
.url
.spec().c_str()));
253 void ShellWindow::AddNewContents(WebContents
* source
,
254 WebContents
* new_contents
,
255 WindowOpenDisposition disposition
,
256 const gfx::Rect
& initial_pos
,
259 DCHECK(Profile::FromBrowserContext(new_contents
->GetBrowserContext()) ==
261 delegate_
->AddNewContents(profile_
, new_contents
, disposition
,
262 initial_pos
, user_gesture
, was_blocked
);
265 void ShellWindow::HandleKeyboardEvent(
267 const content::NativeWebKeyboardEvent
& event
) {
268 native_app_window_
->HandleKeyboardEvent(event
);
271 void ShellWindow::RequestToLockMouse(WebContents
* web_contents
,
273 bool last_unlocked_by_target
) {
274 bool has_permission
= IsExtensionWithPermissionOrSuggestInConsole(
275 APIPermission::kPointerLock
,
277 web_contents
->GetRenderViewHost());
279 web_contents
->GotResponseToLockMouseRequest(has_permission
);
282 void ShellWindow::OnNativeClose() {
283 extensions::ShellWindowRegistry::Get(profile_
)->RemoveShellWindow(this);
284 if (shell_window_contents_
)
285 shell_window_contents_
->NativeWindowClosed();
289 void ShellWindow::OnNativeWindowChanged() {
290 SaveWindowPosition();
291 if (shell_window_contents_
&& native_app_window_
)
292 shell_window_contents_
->NativeWindowChanged(native_app_window_
.get());
295 void ShellWindow::OnNativeWindowActivated() {
296 extensions::ShellWindowRegistry::Get(profile_
)->ShellWindowActivated(this);
299 scoped_ptr
<gfx::Image
> ShellWindow::GetAppListIcon() {
300 // TODO(skuhne): We might want to use LoadImages in UpdateExtensionAppIcon
301 // instead to let the extension give us pre-defined icons in the launcher
302 // and the launcher list sizes. Since there is no mock yet, doing this now
303 // seems a bit premature and we scale for the time being.
304 if (app_icon_
.IsEmpty())
305 return make_scoped_ptr(new gfx::Image());
307 SkBitmap bmp
= skia::ImageOperations::Resize(
308 *app_icon_
.ToSkBitmap(), skia::ImageOperations::RESIZE_BEST
,
309 extension_misc::EXTENSION_ICON_SMALLISH
,
310 extension_misc::EXTENSION_ICON_SMALLISH
);
311 return make_scoped_ptr(
312 new gfx::Image(gfx::ImageSkia::CreateFrom1xBitmap(bmp
)));
315 content::WebContents
* ShellWindow::web_contents() const {
316 return shell_window_contents_
->GetWebContents();
319 NativeAppWindow
* ShellWindow::GetBaseWindow() {
320 return native_app_window_
.get();
323 gfx::NativeWindow
ShellWindow::GetNativeWindow() {
324 return GetBaseWindow()->GetNativeWindow();
327 gfx::Rect
ShellWindow::GetClientBounds() const {
328 gfx::Rect bounds
= native_app_window_
->GetBounds();
329 bounds
.Inset(native_app_window_
->GetFrameInsets());
333 string16
ShellWindow::GetTitle() const {
334 // WebContents::GetTitle() will return the page's URL if there's no <title>
335 // specified. However, we'd prefer to show the name of the extension in that
336 // case, so we directly inspect the NavigationEntry's title.
338 if (!web_contents() ||
339 !web_contents()->GetController().GetActiveEntry() ||
340 web_contents()->GetController().GetActiveEntry()->GetTitle().empty()) {
341 title
= UTF8ToUTF16(extension()->name());
343 title
= web_contents()->GetTitle();
345 const char16 kBadChars
[] = { '\n', 0 };
346 RemoveChars(title
, kBadChars
, &title
);
350 void ShellWindow::SetAppIconUrl(const GURL
& url
) {
351 // Avoid using any previous app icons were are being downloaded.
352 image_loader_ptr_factory_
.InvalidateWeakPtrs();
354 // Reset |app_icon_image_| to abort pending image load (if any).
355 app_icon_image_
.reset();
358 web_contents()->DownloadImage(
360 true, // is a favicon
361 delegate_
->PreferredIconSize(),
362 0, // no maximum size
363 base::Bind(&ShellWindow::DidDownloadFavicon
,
364 image_loader_ptr_factory_
.GetWeakPtr()));
367 void ShellWindow::UpdateDraggableRegions(
368 const std::vector
<extensions::DraggableRegion
>& regions
) {
369 native_app_window_
->UpdateDraggableRegions(regions
);
372 void ShellWindow::UpdateAppIcon(const gfx::Image
& image
) {
376 native_app_window_
->UpdateWindowIcon();
377 extensions::ShellWindowRegistry::Get(profile_
)->ShellWindowIconChanged(this);
380 void ShellWindow::Fullscreen() {
381 fullscreen_for_window_api_
= true;
382 GetBaseWindow()->SetFullscreen(true);
385 void ShellWindow::Maximize() {
386 GetBaseWindow()->Maximize();
389 void ShellWindow::Minimize() {
390 GetBaseWindow()->Minimize();
393 void ShellWindow::Restore() {
394 fullscreen_for_window_api_
= false;
395 fullscreen_for_tab_
= false;
396 if (GetBaseWindow()->IsFullscreenOrPending()) {
397 GetBaseWindow()->SetFullscreen(false);
399 GetBaseWindow()->Restore();
403 //------------------------------------------------------------------------------
406 void ShellWindow::DidDownloadFavicon(int id
,
407 int http_status_code
,
408 const GURL
& image_url
,
410 const std::vector
<SkBitmap
>& bitmaps
) {
411 if (image_url
!= app_icon_url_
|| bitmaps
.empty())
414 // Bitmaps are ordered largest to smallest. Choose the smallest bitmap
415 // whose height >= the preferred size.
416 int largest_index
= 0;
417 for (size_t i
= 1; i
< bitmaps
.size(); ++i
) {
418 if (bitmaps
[i
].height() < delegate_
->PreferredIconSize())
422 const SkBitmap
& largest
= bitmaps
[largest_index
];
423 UpdateAppIcon(gfx::Image::CreateFrom1xBitmap(largest
));
426 void ShellWindow::OnExtensionIconImageChanged(extensions::IconImage
* image
) {
427 DCHECK_EQ(app_icon_image_
.get(), image
);
429 UpdateAppIcon(gfx::Image(app_icon_image_
->image_skia()));
432 void ShellWindow::UpdateExtensionAppIcon() {
433 // Avoid using any previous app icons were are being downloaded.
434 image_loader_ptr_factory_
.InvalidateWeakPtrs();
436 app_icon_image_
.reset(new extensions::IconImage(
439 extensions::IconsInfo::GetIcons(extension()),
440 delegate_
->PreferredIconSize(),
441 extensions::IconsInfo::GetDefaultAppIcon(),
444 // Triggers actual image loading with 1x resources. The 2x resource will
445 // be handled by IconImage class when requested.
446 app_icon_image_
->image_skia().GetRepresentation(ui::SCALE_FACTOR_100P
);
449 void ShellWindow::CloseContents(WebContents
* contents
) {
450 native_app_window_
->Close();
453 bool ShellWindow::ShouldSuppressDialogs() {
457 content::ColorChooser
* ShellWindow::OpenColorChooser(WebContents
* web_contents
,
458 SkColor initial_color
) {
459 return delegate_
->ShowColorChooser(web_contents
, initial_color
);
462 void ShellWindow::RunFileChooser(WebContents
* tab
,
463 const content::FileChooserParams
& params
) {
464 if (window_type_is_panel()) {
465 // Panels can't host a file dialog, abort. TODO(stevenjb): allow file
466 // dialogs to be unhosted but still close with the owning web contents.
468 LOG(WARNING
) << "File dialog opened by panel.";
472 delegate_
->RunFileChooser(tab
, params
);
475 bool ShellWindow::IsPopupOrPanel(const WebContents
* source
) const {
479 void ShellWindow::MoveContents(WebContents
* source
, const gfx::Rect
& pos
) {
480 native_app_window_
->SetBounds(pos
);
483 void ShellWindow::NavigationStateChanged(
484 const content::WebContents
* source
, unsigned changed_flags
) {
485 if (changed_flags
& content::INVALIDATE_TYPE_TITLE
)
486 native_app_window_
->UpdateWindowTitle();
487 else if (changed_flags
& content::INVALIDATE_TYPE_TAB
)
488 native_app_window_
->UpdateWindowIcon();
491 void ShellWindow::ToggleFullscreenModeForTab(content::WebContents
* source
,
492 bool enter_fullscreen
) {
493 if (!IsExtensionWithPermissionOrSuggestInConsole(
494 APIPermission::kFullscreen
,
496 source
->GetRenderViewHost())) {
500 fullscreen_for_tab_
= enter_fullscreen
;
502 if (enter_fullscreen
) {
503 native_app_window_
->SetFullscreen(true);
504 } else if (!fullscreen_for_window_api_
) {
505 native_app_window_
->SetFullscreen(false);
509 bool ShellWindow::IsFullscreenForTabOrPending(
510 const content::WebContents
* source
) const {
511 return fullscreen_for_tab_
;
514 void ShellWindow::Observe(int type
,
515 const content::NotificationSource
& source
,
516 const content::NotificationDetails
& details
) {
518 case content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED
: {
519 // TODO(jianli): once http://crbug.com/123007 is fixed, we'll no longer
520 // need to make the native window (ShellWindowViews specially) update
521 // the clickthrough region for the new RVH.
522 native_app_window_
->RenderViewHostChanged();
525 case chrome::NOTIFICATION_EXTENSION_UNLOADED
: {
526 const extensions::Extension
* unloaded_extension
=
527 content::Details
<extensions::UnloadedExtensionInfo
>(
529 if (extension_
== unloaded_extension
)
530 native_app_window_
->Close();
533 case chrome::NOTIFICATION_APP_TERMINATING
:
534 native_app_window_
->Close();
537 NOTREACHED() << "Received unexpected notification";
541 void ShellWindow::SetWebContentsBlocked(content::WebContents
* web_contents
,
543 delegate_
->SetWebContentsBlocked(web_contents
, blocked
);
546 bool ShellWindow::IsWebContentsVisible(content::WebContents
* web_contents
) {
547 return delegate_
->IsWebContentsVisible(web_contents
);
550 extensions::ActiveTabPermissionGranter
*
551 ShellWindow::GetActiveTabPermissionGranter() {
552 // Shell windows don't support the activeTab permission.
556 WebContentsModalDialogHost
* ShellWindow::GetWebContentsModalDialogHost() {
557 return native_app_window_
.get();
560 void ShellWindow::AddMessageToDevToolsConsole(ConsoleMessageLevel level
,
561 const std::string
& message
) {
562 content::RenderViewHost
* rvh
= web_contents()->GetRenderViewHost();
563 rvh
->Send(new ExtensionMsg_AddMessageToConsole(
564 rvh
->GetRoutingID(), level
, message
));
567 void ShellWindow::SaveWindowPosition() {
568 if (window_key_
.empty())
570 if (!native_app_window_
)
573 ShellWindowGeometryCache
* cache
= ShellWindowGeometryCache::Get(profile());
575 gfx::Rect bounds
= native_app_window_
->GetRestoredBounds();
576 bounds
.Inset(native_app_window_
->GetFrameInsets());
577 gfx::Rect screen_bounds
=
578 gfx::Screen::GetNativeScreen()->GetDisplayMatching(bounds
).work_area();
579 ui::WindowShowState window_state
= native_app_window_
->GetRestoredState();
580 cache
->SaveGeometry(extension()->id(),
587 void ShellWindow::AdjustBoundsToBeVisibleOnScreen(
588 const gfx::Rect
& cached_bounds
,
589 const gfx::Rect
& cached_screen_bounds
,
590 const gfx::Rect
& current_screen_bounds
,
591 const gfx::Size
& minimum_size
,
592 gfx::Rect
* bounds
) const {
593 *bounds
= cached_bounds
;
595 // Reposition and resize the bounds if the cached_screen_bounds is different
596 // from the current screen bounds and the current screen bounds doesn't
597 // completely contain the bounds.
598 if (cached_screen_bounds
!= current_screen_bounds
&&
599 !current_screen_bounds
.Contains(cached_bounds
)) {
601 std::max(minimum_size
.width(),
602 std::min(bounds
->width(), current_screen_bounds
.width())));
604 std::max(minimum_size
.height(),
605 std::min(bounds
->height(), current_screen_bounds
.height())));
607 std::max(current_screen_bounds
.x(),
608 std::min(bounds
->x(),
609 current_screen_bounds
.right() - bounds
->width())));
611 std::max(current_screen_bounds
.y(),
612 std::min(bounds
->y(),
613 current_screen_bounds
.bottom() - bounds
->height())));
618 SkRegion
* ShellWindow::RawDraggableRegionsToSkRegion(
619 const std::vector
<extensions::DraggableRegion
>& regions
) {
620 SkRegion
* sk_region
= new SkRegion
;
621 for (std::vector
<extensions::DraggableRegion
>::const_iterator iter
=
623 iter
!= regions
.end(); ++iter
) {
624 const extensions::DraggableRegion
& region
= *iter
;
628 region
.bounds
.right(),
629 region
.bounds
.bottom(),
630 region
.draggable
? SkRegion::kUnion_Op
: SkRegion::kDifference_Op
);