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