Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 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/ash/launcher/app_shortcut_launcher_item_controller.h" |
| 6 | |
Ben Murdoch | 2385ea3 | 2013-08-06 11:01:04 +0100 | [diff] [blame] | 7 | #include "apps/native_app_window.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 8 | #include "ash/wm/window_util.h" |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 9 | #include "chrome/browser/extensions/extension_process_manager.h" |
| 10 | #include "chrome/browser/extensions/extension_system.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 11 | #include "chrome/browser/favicon/favicon_tab_helper.h" |
| 12 | #include "chrome/browser/ui/ash/launcher/chrome_launcher_app_menu_item.h" |
| 13 | #include "chrome/browser/ui/ash/launcher/chrome_launcher_app_menu_item_tab.h" |
| 14 | #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h" |
| 15 | #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_per_app.h" |
| 16 | #include "chrome/browser/ui/ash/launcher/launcher_item_controller.h" |
| 17 | #include "chrome/browser/ui/browser.h" |
| 18 | #include "chrome/browser/ui/browser_finder.h" |
| 19 | #include "chrome/browser/ui/browser_list.h" |
| 20 | #include "chrome/browser/ui/browser_window.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 21 | #include "chrome/browser/ui/host_desktop.h" |
| 22 | #include "chrome/browser/ui/tabs/tab_strip_model.h" |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 23 | #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 24 | #include "content/public/browser/web_contents.h" |
| 25 | #include "ui/aura/window.h" |
Torne (Richard Coles) | b2df76e | 2013-05-13 16:52:09 +0100 | [diff] [blame] | 26 | #include "ui/base/events/event.h" |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 27 | #include "ui/views/corewm/window_animations.h" |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 28 | |
| 29 | using extensions::Extension; |
| 30 | |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 31 | namespace { |
| 32 | |
| 33 | // The time delta between clicks in which clicks to launch V2 apps are ignored. |
| 34 | const int kClickSuppressionInMS = 1000; |
| 35 | |
| 36 | } // namespace |
| 37 | |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 38 | // Item controller for an app shortcut. Shortcuts track app and launcher ids, |
| 39 | // but do not have any associated windows (opening a shortcut will replace the |
| 40 | // item with the appropriate LauncherItemController type). |
| 41 | AppShortcutLauncherItemController::AppShortcutLauncherItemController( |
| 42 | const std::string& app_id, |
| 43 | ChromeLauncherControllerPerApp* controller) |
| 44 | : LauncherItemController(TYPE_SHORTCUT, app_id, controller), |
| 45 | app_controller_(controller) { |
| 46 | // To detect V1 applications we use their domain and match them against the |
| 47 | // used URL. This will also work with applications like Google Drive. |
| 48 | const Extension* extension = |
| 49 | launcher_controller()->GetExtensionForAppID(app_id); |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 50 | // Some unit tests have no real extension. |
| 51 | if (extension) { |
| 52 | set_refocus_url(GURL( |
| 53 | extensions::AppLaunchInfo::GetLaunchWebURL(extension).spec() + "*")); |
| 54 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 55 | } |
| 56 | |
| 57 | AppShortcutLauncherItemController::~AppShortcutLauncherItemController() { |
| 58 | } |
| 59 | |
| 60 | string16 AppShortcutLauncherItemController::GetTitle() { |
| 61 | return GetAppTitle(); |
| 62 | } |
| 63 | |
| 64 | bool AppShortcutLauncherItemController::HasWindow(aura::Window* window) const { |
| 65 | std::vector<content::WebContents*> content = |
| 66 | app_controller_->GetV1ApplicationsFromAppId(app_id()); |
| 67 | for (size_t i = 0; i < content.size(); i++) { |
| 68 | Browser* browser = chrome::FindBrowserWithWebContents(content[i]); |
| 69 | if (browser && browser->window()->GetNativeWindow() == window) |
| 70 | return true; |
| 71 | } |
| 72 | return false; |
| 73 | } |
| 74 | |
| 75 | bool AppShortcutLauncherItemController::IsOpen() const { |
| 76 | return !app_controller_->GetV1ApplicationsFromAppId(app_id()).empty(); |
| 77 | } |
| 78 | |
| 79 | bool AppShortcutLauncherItemController::IsVisible() const { |
| 80 | // Return true if any browser window associated with the app is visible. |
| 81 | std::vector<content::WebContents*> content = |
| 82 | app_controller_->GetV1ApplicationsFromAppId(app_id()); |
| 83 | for (size_t i = 0; i < content.size(); i++) { |
| 84 | Browser* browser = chrome::FindBrowserWithWebContents(content[i]); |
| 85 | if (browser && browser->window()->GetNativeWindow()->IsVisible()) |
| 86 | return true; |
| 87 | } |
| 88 | return false; |
| 89 | } |
| 90 | |
| 91 | void AppShortcutLauncherItemController::Launch(int event_flags) { |
| 92 | app_controller_->LaunchApp(app_id(), event_flags); |
| 93 | } |
| 94 | |
| 95 | void AppShortcutLauncherItemController::Activate() { |
| 96 | content::WebContents* content = GetLRUApplication(); |
| 97 | if (!content) { |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 98 | if (IsV2App()) { |
| 99 | // Ideally we come here only once. After that ShellLauncherItemController |
| 100 | // will take over when the shell window gets opened. However there are |
| 101 | // apps which take a lot of time for pre-processing (like the files app) |
| 102 | // before they open a window. Since there is currently no other way to |
| 103 | // detect if an app was started we suppress any further clicks within a |
| 104 | // special time out. |
| 105 | if (!AllowNextLaunchAttempt()) |
| 106 | return; |
| 107 | } |
| 108 | Launch(ui::EF_NONE); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 109 | return; |
| 110 | } |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 111 | ActivateContent(content); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 112 | } |
| 113 | |
| 114 | void AppShortcutLauncherItemController::Close() { |
| 115 | // Close all running 'programs' of this type. |
| 116 | std::vector<content::WebContents*> content = |
| 117 | app_controller_->GetV1ApplicationsFromAppId(app_id()); |
| 118 | for (size_t i = 0; i < content.size(); i++) { |
| 119 | Browser* browser = chrome::FindBrowserWithWebContents(content[i]); |
| 120 | if (!browser) |
| 121 | continue; |
| 122 | TabStripModel* tab_strip = browser->tab_strip_model(); |
| 123 | int index = tab_strip->GetIndexOfWebContents(content[i]); |
| 124 | DCHECK(index != TabStripModel::kNoTab); |
| 125 | tab_strip->CloseWebContentsAt(index, TabStripModel::CLOSE_NONE); |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | void AppShortcutLauncherItemController::Clicked(const ui::Event& event) { |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 130 | // In case of a keyboard event, we were called by a hotkey. In that case we |
| 131 | // activate the next item in line if an item of our list is already active. |
| 132 | if (event.type() == ui::ET_KEY_RELEASED) { |
| 133 | if (AdvanceToNextApp()) |
| 134 | return; |
| 135 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 136 | Activate(); |
| 137 | } |
| 138 | |
| 139 | void AppShortcutLauncherItemController::OnRemoved() { |
| 140 | // AppShortcutLauncherItemController is unowned; delete on removal. |
| 141 | delete this; |
| 142 | } |
| 143 | |
| 144 | void AppShortcutLauncherItemController::LauncherItemChanged( |
| 145 | int model_index, |
| 146 | const ash::LauncherItem& old_item) { |
| 147 | } |
| 148 | |
| 149 | ChromeLauncherAppMenuItems |
Torne (Richard Coles) | 90dce4d | 2013-05-29 14:40:03 +0100 | [diff] [blame] | 150 | AppShortcutLauncherItemController::GetApplicationList(int event_flags) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 151 | ChromeLauncherAppMenuItems items; |
| 152 | // Add the application name to the menu. |
| 153 | items.push_back(new ChromeLauncherAppMenuItem(GetTitle(), NULL, false)); |
| 154 | |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 155 | std::vector<content::WebContents*> content_list = GetRunningApplications(); |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 156 | |
| 157 | for (size_t i = 0; i < content_list.size(); i++) { |
| 158 | content::WebContents* web_contents = content_list[i]; |
| 159 | // Get the icon. |
| 160 | gfx::Image app_icon = app_controller_->GetAppListIcon(web_contents); |
| 161 | string16 title = app_controller_->GetAppListTitle(web_contents); |
| 162 | items.push_back(new ChromeLauncherAppMenuItemTab( |
| 163 | title, &app_icon, web_contents, i == 0)); |
| 164 | } |
| 165 | return items.Pass(); |
| 166 | } |
| 167 | |
| 168 | std::vector<content::WebContents*> |
| 169 | AppShortcutLauncherItemController::GetRunningApplications() { |
| 170 | std::vector<content::WebContents*> items; |
| 171 | |
| 172 | URLPattern refocus_pattern(URLPattern::SCHEME_ALL); |
| 173 | refocus_pattern.SetMatchAllURLs(true); |
| 174 | |
| 175 | if (!refocus_url_.is_empty()) { |
| 176 | refocus_pattern.SetMatchAllURLs(false); |
| 177 | refocus_pattern.Parse(refocus_url_.spec()); |
| 178 | } |
| 179 | |
| 180 | const Extension* extension = |
| 181 | launcher_controller()->GetExtensionForAppID(app_id()); |
| 182 | |
| 183 | // It is possible to come here While an extension gets loaded. |
| 184 | if (!extension) |
| 185 | return items; |
| 186 | |
| 187 | const BrowserList* ash_browser_list = |
| 188 | BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH); |
| 189 | for (BrowserList::const_iterator it = ash_browser_list->begin(); |
| 190 | it != ash_browser_list->end(); ++it) { |
| 191 | Browser* browser = *it; |
| 192 | TabStripModel* tab_strip = browser->tab_strip_model(); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 193 | for (int index = 0; index < tab_strip->count(); index++) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 194 | content::WebContents* web_contents = tab_strip->GetWebContentsAt(index); |
| 195 | if (WebContentMatchesApp(extension, refocus_pattern, web_contents)) |
| 196 | items.push_back(web_contents); |
| 197 | } |
| 198 | } |
| 199 | return items; |
| 200 | } |
| 201 | |
| 202 | content::WebContents* AppShortcutLauncherItemController::GetLRUApplication() { |
| 203 | URLPattern refocus_pattern(URLPattern::SCHEME_ALL); |
| 204 | refocus_pattern.SetMatchAllURLs(true); |
| 205 | |
| 206 | if (!refocus_url_.is_empty()) { |
| 207 | refocus_pattern.SetMatchAllURLs(false); |
| 208 | refocus_pattern.Parse(refocus_url_.spec()); |
| 209 | } |
| 210 | |
| 211 | const Extension* extension = |
| 212 | launcher_controller()->GetExtensionForAppID(app_id()); |
| 213 | |
| 214 | // We may get here while the extension is loading (and NULL). |
| 215 | if (!extension) |
| 216 | return NULL; |
| 217 | |
| 218 | const BrowserList* ash_browser_list = |
| 219 | BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_ASH); |
| 220 | for (BrowserList::const_reverse_iterator |
| 221 | it = ash_browser_list->begin_last_active(); |
| 222 | it != ash_browser_list->end_last_active(); ++it) { |
| 223 | Browser* browser = *it; |
| 224 | TabStripModel* tab_strip = browser->tab_strip_model(); |
| 225 | // We start to enumerate from the active index. |
| 226 | int active_index = tab_strip->active_index(); |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 227 | for (int index = 0; index < tab_strip->count(); index++) { |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 228 | content::WebContents* web_contents = tab_strip->GetWebContentsAt( |
| 229 | (index + active_index) % tab_strip->count()); |
| 230 | if (WebContentMatchesApp(extension, refocus_pattern, web_contents)) |
| 231 | return web_contents; |
| 232 | } |
| 233 | } |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 234 | // Coming here our application was not in the LRU list. This could have |
| 235 | // happened because it did never get activated yet. So check the browser list |
| 236 | // as well. |
| 237 | for (BrowserList::const_iterator it = ash_browser_list->begin(); |
| 238 | it != ash_browser_list->end(); ++it) { |
| 239 | Browser* browser = *it; |
| 240 | TabStripModel* tab_strip = browser->tab_strip_model(); |
| 241 | for (int index = 0; index < tab_strip->count(); index++) { |
| 242 | content::WebContents* web_contents = tab_strip->GetWebContentsAt(index); |
| 243 | if (WebContentMatchesApp(extension, refocus_pattern, web_contents)) |
| 244 | return web_contents; |
| 245 | } |
| 246 | } |
Torne (Richard Coles) | 2a99a7e | 2013-03-28 15:31:22 +0000 | [diff] [blame] | 247 | return NULL; |
| 248 | } |
| 249 | |
| 250 | bool AppShortcutLauncherItemController::WebContentMatchesApp( |
| 251 | const extensions::Extension* extension, |
| 252 | const URLPattern& refocus_pattern, |
| 253 | content::WebContents* web_contents) { |
| 254 | const GURL tab_url = web_contents->GetURL(); |
| 255 | // There are three ways to identify the association of a URL with this |
| 256 | // extension: |
| 257 | // - The refocus pattern is matched (needed for apps like drive). |
| 258 | // - The extension's origin + extent gets matched. |
| 259 | // - The launcher controller knows that the tab got created for this app. |
| 260 | return ((!refocus_pattern.match_all_urls() && |
| 261 | refocus_pattern.MatchesURL(tab_url)) || |
| 262 | (extension->OverlapsWithOrigin(tab_url) && |
| 263 | extension->web_extent().MatchesURL(tab_url)) || |
| 264 | launcher_controller()->GetPerAppInterface()-> |
| 265 | IsWebContentHandledByApplication(web_contents, app_id())); |
| 266 | } |
Torne (Richard Coles) | c2e0dbd | 2013-05-09 18:35:53 +0100 | [diff] [blame] | 267 | |
| 268 | void AppShortcutLauncherItemController::ActivateContent( |
| 269 | content::WebContents* content) { |
| 270 | Browser* browser = chrome::FindBrowserWithWebContents(content); |
| 271 | TabStripModel* tab_strip = browser->tab_strip_model(); |
| 272 | int index = tab_strip->GetIndexOfWebContents(content); |
| 273 | DCHECK_NE(TabStripModel::kNoTab, index); |
| 274 | |
| 275 | int old_index = tab_strip->active_index(); |
| 276 | if (index != old_index) |
| 277 | tab_strip->ActivateTabAt(index, false); |
| 278 | app_controller_->ActivateWindowOrMinimizeIfActive(browser->window(), |
| 279 | index == old_index && GetRunningApplications().size() == 1); |
| 280 | } |
| 281 | |
| 282 | bool AppShortcutLauncherItemController::AdvanceToNextApp() { |
| 283 | std::vector<content::WebContents*> items = GetRunningApplications(); |
| 284 | if (items.size() >= 1) { |
| 285 | Browser* browser = chrome::FindBrowserWithWindow( |
| 286 | ash::wm::GetActiveWindow()); |
| 287 | if (browser) { |
| 288 | TabStripModel* tab_strip = browser->tab_strip_model(); |
| 289 | content::WebContents* active = tab_strip->GetWebContentsAt( |
| 290 | tab_strip->active_index()); |
| 291 | std::vector<content::WebContents*>::const_iterator i( |
| 292 | std::find(items.begin(), items.end(), active)); |
| 293 | if (i != items.end()) { |
| 294 | if (items.size() == 1) { |
| 295 | // If there is only a single item available, we animate it upon key |
| 296 | // action. |
| 297 | AnimateWindow(browser->window()->GetNativeWindow(), |
| 298 | views::corewm::WINDOW_ANIMATION_TYPE_BOUNCE); |
| 299 | } else { |
| 300 | int index = (static_cast<int>(i - items.begin()) + 1) % items.size(); |
| 301 | ActivateContent(items[index]); |
| 302 | } |
| 303 | return true; |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | return false; |
| 308 | } |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 309 | |
Ben Murdoch | eb525c5 | 2013-07-10 11:40:50 +0100 | [diff] [blame] | 310 | bool AppShortcutLauncherItemController::IsV2App() { |
| 311 | const Extension* extension = app_controller_->GetExtensionForAppID(app_id()); |
| 312 | return extension && extension->is_platform_app(); |
| 313 | } |
| 314 | |
| 315 | bool AppShortcutLauncherItemController::AllowNextLaunchAttempt() { |
| 316 | if (last_launch_attempt_.is_null() || |
| 317 | last_launch_attempt_ + base::TimeDelta::FromMilliseconds( |
| 318 | kClickSuppressionInMS) < base::Time::Now()) { |
| 319 | last_launch_attempt_ = base::Time::Now(); |
| 320 | return true; |
| 321 | } |
| 322 | return false; |
Torne (Richard Coles) | 868fa2f | 2013-06-11 10:57:03 +0100 | [diff] [blame] | 323 | } |