OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "apps/app_shim/extension_app_shim_handler_mac.h" | |
6 | |
7 #include "apps/app_lifetime_monitor_factory.h" | |
8 #include "apps/app_shim/app_shim_host_manager_mac.h" | |
9 #include "apps/app_shim/app_shim_messages.h" | |
10 #include "apps/launcher.h" | |
11 #include "base/files/file_path.h" | |
12 #include "base/logging.h" | |
13 #include "chrome/browser/browser_process.h" | |
14 #include "chrome/browser/chrome_notification_types.h" | |
15 #include "chrome/browser/profiles/profile.h" | |
16 #include "chrome/browser/profiles/profile_manager.h" | |
17 #include "chrome/browser/ui/extensions/extension_enable_flow.h" | |
18 #include "chrome/browser/ui/extensions/extension_enable_flow_delegate.h" | |
19 #include "chrome/browser/ui/webui/ntp/core_app_launcher_handler.h" | |
20 #include "chrome/browser/web_applications/web_app_mac.h" | |
21 #include "chrome/common/extensions/extension_constants.h" | |
22 #include "components/crx_file/id_util.h" | |
23 #include "content/public/browser/notification_details.h" | |
24 #include "content/public/browser/notification_service.h" | |
25 #include "content/public/browser/notification_source.h" | |
26 #include "extensions/browser/app_window/app_window.h" | |
27 #include "extensions/browser/app_window/app_window_registry.h" | |
28 #include "extensions/browser/app_window/native_app_window.h" | |
29 #include "extensions/browser/extension_host.h" | |
30 #include "extensions/browser/extension_registry.h" | |
31 #include "ui/base/cocoa/focus_window_set.h" | |
32 | |
33 using extensions::AppWindow; | |
34 using extensions::AppWindowRegistry; | |
35 using extensions::ExtensionRegistry; | |
36 | |
37 namespace { | |
38 | |
39 typedef AppWindowRegistry::AppWindowList AppWindowList; | |
40 | |
41 void ProfileLoadedCallback(base::Callback<void(Profile*)> callback, | |
42 Profile* profile, | |
43 Profile::CreateStatus status) { | |
44 if (status == Profile::CREATE_STATUS_INITIALIZED) { | |
45 callback.Run(profile); | |
46 return; | |
47 } | |
48 | |
49 // This should never get an error since it only loads existing profiles. | |
50 DCHECK_EQ(Profile::CREATE_STATUS_CREATED, status); | |
51 } | |
52 | |
53 void SetAppHidden(Profile* profile, const std::string& app_id, bool hidden) { | |
54 AppWindowList windows = | |
55 AppWindowRegistry::Get(profile)->GetAppWindowsForApp(app_id); | |
56 for (AppWindowList::const_reverse_iterator it = windows.rbegin(); | |
57 it != windows.rend(); | |
58 ++it) { | |
59 if (hidden) | |
60 (*it)->GetBaseWindow()->HideWithApp(); | |
61 else | |
62 (*it)->GetBaseWindow()->ShowWithApp(); | |
63 } | |
64 } | |
65 | |
66 bool FocusWindows(const AppWindowList& windows) { | |
67 if (windows.empty()) | |
68 return false; | |
69 | |
70 std::set<gfx::NativeWindow> native_windows; | |
71 for (AppWindowList::const_iterator it = windows.begin(); it != windows.end(); | |
72 ++it) { | |
73 native_windows.insert((*it)->GetNativeWindow()); | |
74 } | |
75 // Allow workspace switching. For the browser process, we can reasonably rely | |
76 // on OS X to switch spaces for us and honor relevant user settings. But shims | |
77 // don't have windows, so we have to do it ourselves. | |
78 ui::FocusWindowSet(native_windows); | |
79 return true; | |
80 } | |
81 | |
82 // Attempts to launch a packaged app, prompting the user to enable it if | |
83 // necessary. The prompt is shown in its own window. | |
84 // This class manages its own lifetime. | |
85 class EnableViaPrompt : public ExtensionEnableFlowDelegate { | |
86 public: | |
87 EnableViaPrompt(Profile* profile, | |
88 const std::string& extension_id, | |
89 const base::Callback<void()>& callback) | |
90 : profile_(profile), | |
91 extension_id_(extension_id), | |
92 callback_(callback) { | |
93 } | |
94 | |
95 virtual ~EnableViaPrompt() { | |
96 } | |
97 | |
98 void Run() { | |
99 flow_.reset(new ExtensionEnableFlow(profile_, extension_id_, this)); | |
100 flow_->StartForCurrentlyNonexistentWindow( | |
101 base::Callback<gfx::NativeWindow(void)>()); | |
102 } | |
103 | |
104 private: | |
105 // ExtensionEnableFlowDelegate overrides. | |
106 virtual void ExtensionEnableFlowFinished() OVERRIDE { | |
107 callback_.Run(); | |
108 delete this; | |
109 } | |
110 | |
111 virtual void ExtensionEnableFlowAborted(bool user_initiated) OVERRIDE { | |
112 callback_.Run(); | |
113 delete this; | |
114 } | |
115 | |
116 Profile* profile_; | |
117 std::string extension_id_; | |
118 base::Callback<void()> callback_; | |
119 scoped_ptr<ExtensionEnableFlow> flow_; | |
120 | |
121 DISALLOW_COPY_AND_ASSIGN(EnableViaPrompt); | |
122 }; | |
123 | |
124 } // namespace | |
125 | |
126 namespace apps { | |
127 | |
128 bool ExtensionAppShimHandler::Delegate::ProfileExistsForPath( | |
129 const base::FilePath& path) { | |
130 ProfileManager* profile_manager = g_browser_process->profile_manager(); | |
131 // Check for the profile name in the profile info cache to ensure that we | |
132 // never access any directory that isn't a known profile. | |
133 base::FilePath full_path = profile_manager->user_data_dir().Append(path); | |
134 ProfileInfoCache& cache = profile_manager->GetProfileInfoCache(); | |
135 return cache.GetIndexOfProfileWithPath(full_path) != std::string::npos; | |
136 } | |
137 | |
138 Profile* ExtensionAppShimHandler::Delegate::ProfileForPath( | |
139 const base::FilePath& path) { | |
140 ProfileManager* profile_manager = g_browser_process->profile_manager(); | |
141 base::FilePath full_path = profile_manager->user_data_dir().Append(path); | |
142 Profile* profile = profile_manager->GetProfileByPath(full_path); | |
143 | |
144 // Use IsValidProfile to check if the profile has been created. | |
145 return profile && profile_manager->IsValidProfile(profile) ? profile : NULL; | |
146 } | |
147 | |
148 void ExtensionAppShimHandler::Delegate::LoadProfileAsync( | |
149 const base::FilePath& path, | |
150 base::Callback<void(Profile*)> callback) { | |
151 ProfileManager* profile_manager = g_browser_process->profile_manager(); | |
152 base::FilePath full_path = profile_manager->user_data_dir().Append(path); | |
153 profile_manager->CreateProfileAsync( | |
154 full_path, | |
155 base::Bind(&ProfileLoadedCallback, callback), | |
156 base::string16(), base::string16(), std::string()); | |
157 } | |
158 | |
159 AppWindowList ExtensionAppShimHandler::Delegate::GetWindows( | |
160 Profile* profile, | |
161 const std::string& extension_id) { | |
162 return AppWindowRegistry::Get(profile)->GetAppWindowsForApp(extension_id); | |
163 } | |
164 | |
165 const extensions::Extension* | |
166 ExtensionAppShimHandler::Delegate::GetAppExtension( | |
167 Profile* profile, | |
168 const std::string& extension_id) { | |
169 ExtensionRegistry* registry = ExtensionRegistry::Get(profile); | |
170 const extensions::Extension* extension = | |
171 registry->GetExtensionById(extension_id, ExtensionRegistry::ENABLED); | |
172 return extension && extension->is_platform_app() ? extension : NULL; | |
173 } | |
174 | |
175 void ExtensionAppShimHandler::Delegate::EnableExtension( | |
176 Profile* profile, | |
177 const std::string& extension_id, | |
178 const base::Callback<void()>& callback) { | |
179 (new EnableViaPrompt(profile, extension_id, callback))->Run(); | |
180 } | |
181 | |
182 void ExtensionAppShimHandler::Delegate::LaunchApp( | |
183 Profile* profile, | |
184 const extensions::Extension* extension, | |
185 const std::vector<base::FilePath>& files) { | |
186 CoreAppLauncherHandler::RecordAppLaunchType( | |
187 extension_misc::APP_LAUNCH_CMD_LINE_APP, extension->GetType()); | |
188 if (files.empty()) { | |
189 apps::LaunchPlatformApp(profile, extension); | |
190 } else { | |
191 for (std::vector<base::FilePath>::const_iterator it = files.begin(); | |
192 it != files.end(); ++it) { | |
193 apps::LaunchPlatformAppWithPath(profile, extension, *it); | |
194 } | |
195 } | |
196 } | |
197 | |
198 void ExtensionAppShimHandler::Delegate::LaunchShim( | |
199 Profile* profile, | |
200 const extensions::Extension* extension) { | |
201 web_app::MaybeLaunchShortcut( | |
202 web_app::ShortcutInfoForExtensionAndProfile(extension, profile)); | |
203 } | |
204 | |
205 void ExtensionAppShimHandler::Delegate::MaybeTerminate() { | |
206 AppShimHandler::MaybeTerminate(); | |
207 } | |
208 | |
209 ExtensionAppShimHandler::ExtensionAppShimHandler() | |
210 : delegate_(new Delegate), | |
211 weak_factory_(this) { | |
212 // This is instantiated in BrowserProcessImpl::PreMainMessageLoopRun with | |
213 // AppShimHostManager. Since PROFILE_CREATED is not fired until | |
214 // ProfileManager::GetLastUsedProfile/GetLastOpenedProfiles, this should catch | |
215 // notifications for all profiles. | |
216 registrar_.Add(this, chrome::NOTIFICATION_PROFILE_CREATED, | |
217 content::NotificationService::AllBrowserContextsAndSources()); | |
218 registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED, | |
219 content::NotificationService::AllBrowserContextsAndSources()); | |
220 } | |
221 | |
222 ExtensionAppShimHandler::~ExtensionAppShimHandler() {} | |
223 | |
224 AppShimHandler::Host* ExtensionAppShimHandler::FindHost( | |
225 Profile* profile, | |
226 const std::string& app_id) { | |
227 HostMap::iterator it = hosts_.find(make_pair(profile, app_id)); | |
228 return it == hosts_.end() ? NULL : it->second; | |
229 } | |
230 | |
231 // static | |
232 void ExtensionAppShimHandler::QuitAppForWindow(AppWindow* app_window) { | |
233 ExtensionAppShimHandler* handler = GetInstance(); | |
234 Host* host = handler->FindHost( | |
235 Profile::FromBrowserContext(app_window->browser_context()), | |
236 app_window->extension_id()); | |
237 if (host) { | |
238 handler->OnShimQuit(host); | |
239 } else { | |
240 // App shims might be disabled or the shim is still starting up. | |
241 AppWindowRegistry::Get( | |
242 Profile::FromBrowserContext(app_window->browser_context())) | |
243 ->CloseAllAppWindowsForApp(app_window->extension_id()); | |
244 } | |
245 } | |
246 | |
247 void ExtensionAppShimHandler::HideAppForWindow(AppWindow* app_window) { | |
248 ExtensionAppShimHandler* handler = GetInstance(); | |
249 Profile* profile = Profile::FromBrowserContext(app_window->browser_context()); | |
250 Host* host = handler->FindHost(profile, app_window->extension_id()); | |
251 if (host) | |
252 host->OnAppHide(); | |
253 else | |
254 SetAppHidden(profile, app_window->extension_id(), true); | |
255 } | |
256 | |
257 void ExtensionAppShimHandler::FocusAppForWindow(AppWindow* app_window) { | |
258 ExtensionAppShimHandler* handler = GetInstance(); | |
259 Profile* profile = Profile::FromBrowserContext(app_window->browser_context()); | |
260 const std::string& app_id = app_window->extension_id(); | |
261 Host* host = handler->FindHost(profile, app_id); | |
262 if (host) { | |
263 handler->OnShimFocus(host, | |
264 APP_SHIM_FOCUS_NORMAL, | |
265 std::vector<base::FilePath>()); | |
266 } else { | |
267 FocusWindows(AppWindowRegistry::Get(profile)->GetAppWindowsForApp(app_id)); | |
268 } | |
269 } | |
270 | |
271 // static | |
272 bool ExtensionAppShimHandler::ActivateAndRequestUserAttentionForWindow( | |
273 AppWindow* app_window) { | |
274 ExtensionAppShimHandler* handler = GetInstance(); | |
275 Profile* profile = Profile::FromBrowserContext(app_window->browser_context()); | |
276 Host* host = handler->FindHost(profile, app_window->extension_id()); | |
277 if (host) { | |
278 // Bring the window to the front without showing it. | |
279 AppWindowRegistry::Get(profile)->AppWindowActivated(app_window); | |
280 host->OnAppRequestUserAttention(APP_SHIM_ATTENTION_INFORMATIONAL); | |
281 return true; | |
282 } else { | |
283 // Just show the app. | |
284 SetAppHidden(profile, app_window->extension_id(), false); | |
285 return false; | |
286 } | |
287 } | |
288 | |
289 // static | |
290 void ExtensionAppShimHandler::RequestUserAttentionForWindow( | |
291 AppWindow* app_window, | |
292 AppShimAttentionType attention_type) { | |
293 ExtensionAppShimHandler* handler = GetInstance(); | |
294 Profile* profile = Profile::FromBrowserContext(app_window->browser_context()); | |
295 Host* host = handler->FindHost(profile, app_window->extension_id()); | |
296 if (host) | |
297 host->OnAppRequestUserAttention(attention_type); | |
298 } | |
299 | |
300 // static | |
301 void ExtensionAppShimHandler::OnChromeWillHide() { | |
302 // Send OnAppHide to all the shims so that they go into the hidden state. | |
303 // This is necessary so that when the shim is next focused, it will know to | |
304 // unhide. | |
305 ExtensionAppShimHandler* handler = GetInstance(); | |
306 for (HostMap::iterator it = handler->hosts_.begin(); | |
307 it != handler->hosts_.end(); | |
308 ++it) { | |
309 it->second->OnAppHide(); | |
310 } | |
311 } | |
312 | |
313 void ExtensionAppShimHandler::OnShimLaunch( | |
314 Host* host, | |
315 AppShimLaunchType launch_type, | |
316 const std::vector<base::FilePath>& files) { | |
317 const std::string& app_id = host->GetAppId(); | |
318 DCHECK(crx_file::id_util::IdIsValid(app_id)); | |
319 | |
320 const base::FilePath& profile_path = host->GetProfilePath(); | |
321 DCHECK(!profile_path.empty()); | |
322 | |
323 if (!delegate_->ProfileExistsForPath(profile_path)) { | |
324 // User may have deleted the profile this shim was originally created for. | |
325 // TODO(jackhou): Add some UI for this case and remove the LOG. | |
326 LOG(ERROR) << "Requested directory is not a known profile '" | |
327 << profile_path.value() << "'."; | |
328 host->OnAppLaunchComplete(APP_SHIM_LAUNCH_PROFILE_NOT_FOUND); | |
329 return; | |
330 } | |
331 | |
332 Profile* profile = delegate_->ProfileForPath(profile_path); | |
333 | |
334 if (profile) { | |
335 OnProfileLoaded(host, launch_type, files, profile); | |
336 return; | |
337 } | |
338 | |
339 // If the profile is not loaded, this must have been a launch by the shim. | |
340 // Load the profile asynchronously, the host will be registered in | |
341 // OnProfileLoaded. | |
342 DCHECK_EQ(APP_SHIM_LAUNCH_NORMAL, launch_type); | |
343 delegate_->LoadProfileAsync( | |
344 profile_path, | |
345 base::Bind(&ExtensionAppShimHandler::OnProfileLoaded, | |
346 weak_factory_.GetWeakPtr(), | |
347 host, launch_type, files)); | |
348 | |
349 // Return now. OnAppLaunchComplete will be called when the app is activated. | |
350 } | |
351 | |
352 // static | |
353 ExtensionAppShimHandler* ExtensionAppShimHandler::GetInstance() { | |
354 return g_browser_process->platform_part() | |
355 ->app_shim_host_manager() | |
356 ->extension_app_shim_handler(); | |
357 } | |
358 | |
359 void ExtensionAppShimHandler::OnProfileLoaded( | |
360 Host* host, | |
361 AppShimLaunchType launch_type, | |
362 const std::vector<base::FilePath>& files, | |
363 Profile* profile) { | |
364 const std::string& app_id = host->GetAppId(); | |
365 | |
366 // The first host to claim this (profile, app_id) becomes the main host. | |
367 // For any others, focus or relaunch the app. | |
368 if (!hosts_.insert(make_pair(make_pair(profile, app_id), host)).second) { | |
369 OnShimFocus(host, | |
370 launch_type == APP_SHIM_LAUNCH_NORMAL ? | |
371 APP_SHIM_FOCUS_REOPEN : APP_SHIM_FOCUS_NORMAL, | |
372 files); | |
373 host->OnAppLaunchComplete(APP_SHIM_LAUNCH_DUPLICATE_HOST); | |
374 return; | |
375 } | |
376 | |
377 if (launch_type != APP_SHIM_LAUNCH_NORMAL) { | |
378 host->OnAppLaunchComplete(APP_SHIM_LAUNCH_SUCCESS); | |
379 return; | |
380 } | |
381 | |
382 // TODO(jeremya): Handle the case that launching the app fails. Probably we | |
383 // need to watch for 'app successfully launched' or at least 'background page | |
384 // exists/was created' and time out with failure if we don't see that sign of | |
385 // life within a certain window. | |
386 const extensions::Extension* extension = | |
387 delegate_->GetAppExtension(profile, app_id); | |
388 if (extension) { | |
389 delegate_->LaunchApp(profile, extension, files); | |
390 return; | |
391 } | |
392 | |
393 delegate_->EnableExtension( | |
394 profile, app_id, | |
395 base::Bind(&ExtensionAppShimHandler::OnExtensionEnabled, | |
396 weak_factory_.GetWeakPtr(), | |
397 host->GetProfilePath(), app_id, files)); | |
398 } | |
399 | |
400 void ExtensionAppShimHandler::OnExtensionEnabled( | |
401 const base::FilePath& profile_path, | |
402 const std::string& app_id, | |
403 const std::vector<base::FilePath>& files) { | |
404 Profile* profile = delegate_->ProfileForPath(profile_path); | |
405 if (!profile) | |
406 return; | |
407 | |
408 const extensions::Extension* extension = | |
409 delegate_->GetAppExtension(profile, app_id); | |
410 if (!extension || !delegate_->ProfileExistsForPath(profile_path)) { | |
411 // If !extension, the extension doesn't exist, or was not re-enabled. | |
412 // If the profile doesn't exist, it may have been deleted during the enable | |
413 // prompt. In this case, NOTIFICATION_PROFILE_DESTROYED may not be fired | |
414 // until later, so respond to the host now. | |
415 Host* host = FindHost(profile, app_id); | |
416 if (host) | |
417 host->OnAppLaunchComplete(APP_SHIM_LAUNCH_APP_NOT_FOUND); | |
418 return; | |
419 } | |
420 | |
421 delegate_->LaunchApp(profile, extension, files); | |
422 } | |
423 | |
424 | |
425 void ExtensionAppShimHandler::OnShimClose(Host* host) { | |
426 // This might be called when shutting down. Don't try to look up the profile | |
427 // since profile_manager might not be around. | |
428 for (HostMap::iterator it = hosts_.begin(); it != hosts_.end(); ) { | |
429 HostMap::iterator current = it++; | |
430 if (current->second == host) | |
431 hosts_.erase(current); | |
432 } | |
433 } | |
434 | |
435 void ExtensionAppShimHandler::OnShimFocus( | |
436 Host* host, | |
437 AppShimFocusType focus_type, | |
438 const std::vector<base::FilePath>& files) { | |
439 DCHECK(delegate_->ProfileExistsForPath(host->GetProfilePath())); | |
440 Profile* profile = delegate_->ProfileForPath(host->GetProfilePath()); | |
441 | |
442 const AppWindowList windows = | |
443 delegate_->GetWindows(profile, host->GetAppId()); | |
444 bool windows_focused = FocusWindows(windows); | |
445 | |
446 if (focus_type == APP_SHIM_FOCUS_NORMAL || | |
447 (focus_type == APP_SHIM_FOCUS_REOPEN && windows_focused)) { | |
448 return; | |
449 } | |
450 | |
451 const extensions::Extension* extension = | |
452 delegate_->GetAppExtension(profile, host->GetAppId()); | |
453 if (extension) { | |
454 delegate_->LaunchApp(profile, extension, files); | |
455 } else { | |
456 // Extensions may have been uninstalled or disabled since the shim | |
457 // started. | |
458 host->OnAppClosed(); | |
459 } | |
460 } | |
461 | |
462 void ExtensionAppShimHandler::OnShimSetHidden(Host* host, bool hidden) { | |
463 DCHECK(delegate_->ProfileExistsForPath(host->GetProfilePath())); | |
464 Profile* profile = delegate_->ProfileForPath(host->GetProfilePath()); | |
465 | |
466 SetAppHidden(profile, host->GetAppId(), hidden); | |
467 } | |
468 | |
469 void ExtensionAppShimHandler::OnShimQuit(Host* host) { | |
470 DCHECK(delegate_->ProfileExistsForPath(host->GetProfilePath())); | |
471 Profile* profile = delegate_->ProfileForPath(host->GetProfilePath()); | |
472 | |
473 const std::string& app_id = host->GetAppId(); | |
474 const AppWindowList windows = delegate_->GetWindows(profile, app_id); | |
475 for (AppWindowRegistry::const_iterator it = windows.begin(); | |
476 it != windows.end(); | |
477 ++it) { | |
478 (*it)->GetBaseWindow()->Close(); | |
479 } | |
480 // Once the last window closes, flow will end up in OnAppDeactivated via | |
481 // AppLifetimeMonitor. | |
482 } | |
483 | |
484 void ExtensionAppShimHandler::set_delegate(Delegate* delegate) { | |
485 delegate_.reset(delegate); | |
486 } | |
487 | |
488 void ExtensionAppShimHandler::Observe( | |
489 int type, | |
490 const content::NotificationSource& source, | |
491 const content::NotificationDetails& details) { | |
492 Profile* profile = content::Source<Profile>(source).ptr(); | |
493 if (profile->IsOffTheRecord()) | |
494 return; | |
495 | |
496 switch (type) { | |
497 case chrome::NOTIFICATION_PROFILE_CREATED: { | |
498 AppLifetimeMonitorFactory::GetForProfile(profile)->AddObserver(this); | |
499 break; | |
500 } | |
501 case chrome::NOTIFICATION_PROFILE_DESTROYED: { | |
502 AppLifetimeMonitorFactory::GetForProfile(profile)->RemoveObserver(this); | |
503 // Shut down every shim associated with this profile. | |
504 for (HostMap::iterator it = hosts_.begin(); it != hosts_.end(); ) { | |
505 // Increment the iterator first as OnAppClosed may call back to | |
506 // OnShimClose and invalidate the iterator. | |
507 HostMap::iterator current = it++; | |
508 if (profile->IsSameProfile(current->first.first)) { | |
509 Host* host = current->second; | |
510 host->OnAppClosed(); | |
511 } | |
512 } | |
513 break; | |
514 } | |
515 default: { | |
516 NOTREACHED(); // Unexpected notification. | |
517 break; | |
518 } | |
519 } | |
520 } | |
521 | |
522 void ExtensionAppShimHandler::OnAppStart(Profile* profile, | |
523 const std::string& app_id) {} | |
524 | |
525 void ExtensionAppShimHandler::OnAppActivated(Profile* profile, | |
526 const std::string& app_id) { | |
527 const extensions::Extension* extension = | |
528 delegate_->GetAppExtension(profile, app_id); | |
529 if (!extension) | |
530 return; | |
531 | |
532 Host* host = FindHost(profile, app_id); | |
533 if (host) { | |
534 host->OnAppLaunchComplete(APP_SHIM_LAUNCH_SUCCESS); | |
535 OnShimFocus(host, APP_SHIM_FOCUS_NORMAL, std::vector<base::FilePath>()); | |
536 return; | |
537 } | |
538 | |
539 delegate_->LaunchShim(profile, extension); | |
540 } | |
541 | |
542 void ExtensionAppShimHandler::OnAppDeactivated(Profile* profile, | |
543 const std::string& app_id) { | |
544 Host* host = FindHost(profile, app_id); | |
545 if (host) | |
546 host->OnAppClosed(); | |
547 | |
548 if (hosts_.empty()) | |
549 delegate_->MaybeTerminate(); | |
550 } | |
551 | |
552 void ExtensionAppShimHandler::OnAppStop(Profile* profile, | |
553 const std::string& app_id) {} | |
554 | |
555 void ExtensionAppShimHandler::OnChromeTerminating() {} | |
556 | |
557 } // namespace apps | |
OLD | NEW |