1 // Copyright 2019 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_service/app_service_instance_registry_helper.h"
6
7 #include <set>
8 #include <string>
9 #include <vector>
10
11 #include "base/stl_util.h"
12 #include "base/time/time.h"
13 #include "chrome/browser/apps/app_service/app_service_proxy.h"
14 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
15 #include "chrome/browser/chromeos/crostini/crostini_shelf_utils.h"
16 #include "chrome/browser/chromeos/crostini/crostini_util.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/profiles/profile_manager.h"
19 #include "chrome/browser/ui/ash/launcher/app_service/app_service_app_window_launcher_controller.h"
20 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.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/web_applications/components/web_app_id.h"
25 #include "components/services/app_service/public/cpp/instance_update.h"
26 #include "components/services/app_service/public/mojom/types.mojom.h"
27 #include "content/public/browser/web_contents.h"
28 #include "extensions/common/constants.h"
29 #include "ui/wm/core/window_util.h"
30 #include "ui/wm/public/activation_client.h"
31
AppServiceInstanceRegistryHelper(AppServiceAppWindowLauncherController * controller)32 AppServiceInstanceRegistryHelper::AppServiceInstanceRegistryHelper(
33 AppServiceAppWindowLauncherController* controller)
34 : controller_(controller),
35 proxy_(apps::AppServiceProxyFactory::GetForProfile(
36 controller->owner()->profile())),
37 launcher_controller_helper_(std::make_unique<LauncherControllerHelper>(
38 controller->owner()->profile())) {
39 DCHECK(controller_);
40 }
41
42 AppServiceInstanceRegistryHelper::~AppServiceInstanceRegistryHelper() = default;
43
ActiveUserChanged()44 void AppServiceInstanceRegistryHelper::ActiveUserChanged() {
45 proxy_ = apps::AppServiceProxyFactory::GetForProfile(
46 ProfileManager::GetActiveUserProfile());
47 }
48
AdditionalUserAddedToSession(Profile * profile)49 void AppServiceInstanceRegistryHelper::AdditionalUserAddedToSession(
50 Profile* profile) {
51 proxy_ = apps::AppServiceProxyFactory::GetForProfile(profile);
52 }
53
OnActiveTabChanged(content::WebContents * old_contents,content::WebContents * new_contents)54 void AppServiceInstanceRegistryHelper::OnActiveTabChanged(
55 content::WebContents* old_contents,
56 content::WebContents* new_contents) {
57 if (old_contents) {
58 auto* window = old_contents->GetNativeView();
59
60 // Get the app_id from the existed instance first. If there is no record for
61 // the window, get the app_id from contents. Because when Chrome app open
62 // method is changed from windows to tabs, the app_id could be changed based
63 // on the URL, e.g. google photos, which might cause instance app_id
64 // inconsistent DCHECK error.
65 std::string app_id = GetAppId(window);
66 if (app_id.empty())
67 app_id = launcher_controller_helper_->GetAppID(old_contents);
68
69 // If app_id is empty, we should not set it as inactive because this is
70 // Chrome's tab.
71 if (!app_id.empty()) {
72 apps::InstanceState state = GetState(window);
73 // If the app has been inactive, we don't need to update the instance.
74 if ((state & apps::InstanceState::kActive) !=
75 apps::InstanceState::kUnknown) {
76 state = static_cast<apps::InstanceState>(state &
77 ~apps::InstanceState::kActive);
78 OnInstances(app_id, GetWindow(old_contents), std::string(), state);
79 }
80 }
81 }
82
83 if (new_contents) {
84 auto* window = GetWindow(new_contents);
85
86 // Get the app_id from the existed instance first. If there is no record for
87 // the window, get the app_id from contents. Because when Chrome app open
88 // method is changed from windows to tabs, the app_id could be changed based
89 // on the URL, e.g. google photos, which might cause instance app_id
90 // inconsistent DCHECK error.
91 std::string app_id = GetAppId(window);
92 if (app_id.empty())
93 app_id = GetAppId(new_contents);
94
95 // When the user drags a tab to a new browser, or to an other browser, the
96 // top window could be changed, so the relation for the tap window and the
97 // browser window.
98 UpdateTabWindow(app_id, window);
99
100 // If the app is active, it should be started, running, and visible.
101 apps::InstanceState state = static_cast<apps::InstanceState>(
102 apps::InstanceState::kStarted | apps::InstanceState::kRunning |
103 apps::InstanceState::kActive | apps::InstanceState::kVisible);
104 OnInstances(app_id, window, std::string(), state);
105 }
106 }
107
OnTabReplaced(content::WebContents * old_contents,content::WebContents * new_contents)108 void AppServiceInstanceRegistryHelper::OnTabReplaced(
109 content::WebContents* old_contents,
110 content::WebContents* new_contents) {
111 OnTabClosing(old_contents);
112 OnTabInserted(new_contents);
113 }
114
OnTabInserted(content::WebContents * contents)115 void AppServiceInstanceRegistryHelper::OnTabInserted(
116 content::WebContents* contents) {
117 std::string app_id = GetAppId(contents);
118 aura::Window* window = GetWindow(contents);
119
120 // When the user drags a tab to a new browser, or to an other browser, it
121 // could generate a temp instance for this window with the Chrome application
122 // app_id. For this case, this temp instance can be deleted, otherwise, DCHECK
123 // error for inconsistent app_id.
124 const std::string old_app_id = GetAppId(window);
125 if (!old_app_id.empty() && app_id != old_app_id) {
126 RemoveTabWindow(old_app_id, window);
127 OnInstances(old_app_id, window, std::string(),
128 apps::InstanceState::kDestroyed);
129 }
130
131 // The tab window could be dragged to a new browser, and the top window could
132 // be changed, so clear the old top window first, then add the new top window.
133 UpdateTabWindow(app_id, window);
134 apps::InstanceState state = static_cast<apps::InstanceState>(
135 apps::InstanceState::kStarted | apps::InstanceState::kRunning);
136
137 // Observe the tab, because when the system is shutdown or some other cases,
138 // the window could be destroyed without calling OnTabClosing. So observe the
139 // tab to get the notify when the window is destroyed.
140 controller_->ObserveWindow(window);
141 OnInstances(app_id, window, std::string(), state);
142 }
143
OnTabClosing(content::WebContents * contents)144 void AppServiceInstanceRegistryHelper::OnTabClosing(
145 content::WebContents* contents) {
146 aura::Window* window = GetWindow(contents);
147
148 // When the tab is closed, if the window does not exists in the AppService
149 // InstanceRegistry, we don't need to update the status.
150 const std::string app_id = GetAppId(window);
151 if (app_id.empty())
152 return;
153
154 RemoveTabWindow(app_id, window);
155 OnInstances(app_id, window, std::string(), apps::InstanceState::kDestroyed);
156 }
157
OnBrowserRemoved()158 void AppServiceInstanceRegistryHelper::OnBrowserRemoved() {
159 auto windows = GetWindows(extension_misc::kChromeAppId);
160 for (auto* window : windows) {
161 if (!chrome::FindBrowserWithWindow(window)) {
162 // Remove windows from |browser_window_to_tab_window_| and
163 // |tab_window_to_browser_window_|, because OnTabClosing could be not
164 // called for tabs in the browser, when the browser is removed.
165 if (base::Contains(browser_window_to_tab_window_, window)) {
166 for (auto* w : browser_window_to_tab_window_[window]) {
167 tab_window_to_browser_window_.erase(w);
168 OnInstances(GetAppId(w), w, std::string(),
169 apps::InstanceState::kDestroyed);
170 }
171 browser_window_to_tab_window_.erase(window);
172 }
173
174 // The browser is removed if the window can't be found, so update the
175 // Chrome window instance as destroyed.
176 OnInstances(extension_misc::kChromeAppId, window, std::string(),
177 apps::InstanceState::kDestroyed);
178 }
179 }
180 }
181
OnInstances(const std::string & app_id,aura::Window * window,const std::string & launch_id,apps::InstanceState state)182 void AppServiceInstanceRegistryHelper::OnInstances(const std::string& app_id,
183 aura::Window* window,
184 const std::string& launch_id,
185 apps::InstanceState state) {
186 if (app_id.empty() || !window)
187 return;
188
189 // If the window is not observed, this means the window is being destroyed. In
190 // this case, don't add the instance because we might keep the record for the
191 // destroyed window, which could cause crash.
192 if (state != apps::InstanceState::kDestroyed &&
193 !controller_->IsObservingWindow(window)) {
194 state = apps::InstanceState::kDestroyed;
195 }
196
197 std::unique_ptr<apps::Instance> instance =
198 std::make_unique<apps::Instance>(app_id, window);
199 instance->SetLaunchId(launch_id);
200 instance->UpdateState(state, base::Time::Now());
201
202 std::vector<std::unique_ptr<apps::Instance>> deltas;
203 deltas.push_back(std::move(instance));
204
205 // The window could be teleported from the inactive user's profile to the
206 // current active user, so search all proxies. If the instance is found from a
207 // proxy, still save to that proxy, otherwise, save to the current active user
208 // profile's proxy.
209 apps::AppServiceProxy* proxy = proxy_;
210 for (auto* profile : controller_->GetProfileList()) {
211 auto* proxy_for_profile =
212 apps::AppServiceProxyFactory::GetForProfile(profile);
213 if (proxy_for_profile->InstanceRegistry().Exists(window)) {
214 proxy = proxy_for_profile;
215 break;
216 }
217 }
218 proxy->InstanceRegistry().OnInstances(std::move(deltas));
219 }
220
OnSetShelfIDForBrowserWindowContents(content::WebContents * contents)221 void AppServiceInstanceRegistryHelper::OnSetShelfIDForBrowserWindowContents(
222 content::WebContents* contents) {
223 aura::Window* window = GetWindow(contents);
224 if (!window || !window->GetToplevelWindow())
225 return;
226
227 // If the app id is changed, call OnTabInserted to remove the old app id in
228 // AppService InstanceRegistry, and insert the new app id.
229 std::string app_id = GetAppId(contents);
230 const std::string old_app_id = GetAppId(window);
231 if (app_id != old_app_id)
232 OnTabInserted(contents);
233
234 // When system startup, session restore creates windows before
235 // ChromeLauncherController is created, so windows restored can’t get the
236 // visible and activated status from OnWindowVisibilityChanged and
237 // OnWindowActivated. Also web apps are ready at the very late phase which
238 // delays the shelf id setting for windows. So check the top window's visible
239 // and activated status when we have the shelf id.
240 window = window->GetToplevelWindow();
241 const std::string top_app_id = GetAppId(window);
242 if (!top_app_id.empty())
243 app_id = top_app_id;
244 OnWindowVisibilityChanged(ash::ShelfID(app_id), window, window->IsVisible());
245 auto* client = wm::GetActivationClient(window->GetRootWindow());
246 if (client) {
247 SetWindowActivated(ash::ShelfID(app_id), window,
248 /*active*/ window == client->GetActiveWindow());
249 }
250 }
251
OnWindowVisibilityChanged(const ash::ShelfID & shelf_id,aura::Window * window,bool visible)252 void AppServiceInstanceRegistryHelper::OnWindowVisibilityChanged(
253 const ash::ShelfID& shelf_id,
254 aura::Window* window,
255 bool visible) {
256 if (shelf_id.app_id != extension_misc::kChromeAppId) {
257 // For Web apps opened in an app window, we need to find the top level
258 // window to compare with the parameter |window|, because we save the tab
259 // window in AppService InstanceRegistry for Web apps, and we should set the
260 // state for the tab window to keep one instance for the Web app.
261 auto windows = GetWindows(shelf_id.app_id);
262 for (auto* it : windows) {
263 auto tab_it = tab_window_to_browser_window_.find(it);
264 if (tab_it == tab_window_to_browser_window_.end() ||
265 tab_it->second != window) {
266 continue;
267 }
268
269 // When the user drags a tab to a new browser, or to an other browser, the
270 // top window could be changed, so update the relation for the tap window
271 // and the browser window.
272 UpdateTabWindow(shelf_id.app_id, it);
273
274 apps::InstanceState state = CalculateVisibilityState(it, visible);
275 OnInstances(shelf_id.app_id, it, shelf_id.launch_id, state);
276 return;
277 }
278 return;
279 }
280
281 apps::InstanceState state = CalculateVisibilityState(window, visible);
282 OnInstances(extension_misc::kChromeAppId, window, std::string(), state);
283
284 if (!base::Contains(browser_window_to_tab_window_, window))
285 return;
286
287 // For Chrome browser app windows, sets the state for each tab window instance
288 // in this browser.
289 for (auto* it : browser_window_to_tab_window_[window]) {
290 const std::string app_id = GetAppId(it);
291 if (app_id.empty())
292 continue;
293 apps::InstanceState state = CalculateVisibilityState(it, visible);
294 OnInstances(app_id, it, std::string(), state);
295 }
296 }
297
SetWindowActivated(const ash::ShelfID & shelf_id,aura::Window * window,bool active)298 void AppServiceInstanceRegistryHelper::SetWindowActivated(
299 const ash::ShelfID& shelf_id,
300 aura::Window* window,
301 bool active) {
302 if (shelf_id.app_id != extension_misc::kChromeAppId) {
303 // For Web apps opened in an app window, we need to find the top level
304 // window to compare with |window|, because we save the tab
305 // window in AppService InstanceRegistry for Web apps, and we should set the
306 // state for the tab window to keep one instance for the Web app.
307 auto windows = GetWindows(shelf_id.app_id);
308 for (auto* it : windows) {
309 if (it->GetToplevelWindow() != window)
310 continue;
311
312 // When the user drags a tab to a new browser, or to an other browser, the
313 // top window could be changed, so the relation for the tap window and the
314 // browser window.
315 UpdateTabWindow(shelf_id.app_id, it);
316
317 apps::InstanceState state = CalculateActivatedState(it, active);
318 OnInstances(shelf_id.app_id, it, shelf_id.launch_id, state);
319 return;
320 }
321 return;
322 }
323
324 apps::InstanceState state = CalculateActivatedState(window, active);
325 OnInstances(extension_misc::kChromeAppId, window, std::string(), state);
326
327 if (!base::Contains(browser_window_to_tab_window_, window))
328 return;
329
330 // For the Chrome browser, when the window is activated, the active tab is set
331 // as started, running, visible and active state.
332 if (active) {
333 Browser* browser = chrome::FindBrowserWithWindow(window);
334 if (!browser)
335 return;
336
337 content::WebContents* contents =
338 browser->tab_strip_model()->GetActiveWebContents();
339 if (!contents)
340 return;
341
342 apps::InstanceState state = static_cast<apps::InstanceState>(
343 apps::InstanceState::kStarted | apps::InstanceState::kRunning |
344 apps::InstanceState::kActive | apps::InstanceState::kVisible);
345 auto* contents_window = GetWindow(contents);
346
347 // Get the app_id from the existed instance first. The app_id for PWAs could
348 // be changed based on the URL, e.g. google photos, which might cause
349 // instance app_id inconsistent DCHECK error.
350 std::string app_id = GetAppId(contents_window);
351 app_id = app_id.empty() ? GetAppId(contents) : app_id;
352
353 // When the user drags a tab to a new browser, or to an other browser, the
354 // top window could be changed, so the relation for the tap window and the
355 // browser window.
356 UpdateTabWindow(app_id, contents_window);
357
358 OnInstances(app_id, contents_window, std::string(), state);
359 return;
360 }
361
362 // For Chrome browser app windows, sets the state for each tab window instance
363 // in this browser.
364 for (auto* it : browser_window_to_tab_window_[window]) {
365 const std::string app_id = GetAppId(it);
366 if (app_id.empty())
367 continue;
368 apps::InstanceState state = CalculateActivatedState(it, active);
369 OnInstances(app_id, it, std::string(), state);
370 }
371 }
372
CalculateVisibilityState(aura::Window * window,bool visible) const373 apps::InstanceState AppServiceInstanceRegistryHelper::CalculateVisibilityState(
374 aura::Window* window,
375 bool visible) const {
376 apps::InstanceState state = GetState(window);
377 state = static_cast<apps::InstanceState>(
378 state | apps::InstanceState::kStarted | apps::InstanceState::kRunning);
379 state = (visible) ? static_cast<apps::InstanceState>(
380 state | apps::InstanceState::kVisible)
381 : static_cast<apps::InstanceState>(
382 state & ~(apps::InstanceState::kVisible));
383 return state;
384 }
385
CalculateActivatedState(aura::Window * window,bool active) const386 apps::InstanceState AppServiceInstanceRegistryHelper::CalculateActivatedState(
387 aura::Window* window,
388 bool active) const {
389 // If the app is active, it should be started, running, and visible.
390 if (active) {
391 return static_cast<apps::InstanceState>(
392 apps::InstanceState::kStarted | apps::InstanceState::kRunning |
393 apps::InstanceState::kActive | apps::InstanceState::kVisible);
394 }
395
396 apps::InstanceState state = GetState(window);
397 state = static_cast<apps::InstanceState>(
398 state | apps::InstanceState::kStarted | apps::InstanceState::kRunning);
399 state =
400 static_cast<apps::InstanceState>(state & ~apps::InstanceState::kActive);
401 return state;
402 }
403
IsOpenedInBrowser(const std::string & app_id,aura::Window * window) const404 bool AppServiceInstanceRegistryHelper::IsOpenedInBrowser(
405 const std::string& app_id,
406 aura::Window* window) const {
407 // Crostini Terminal App with the app_id kCrostiniTerminalSystemAppId is a
408 // System Web App.
409 if (app_id == crostini::kCrostiniTerminalSystemAppId)
410 return true;
411
412 if (crostini::IsUnmatchedCrostiniShelfAppId(app_id))
413 return false;
414
415 for (auto* profile : controller_->GetProfileList()) {
416 auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
417 apps::mojom::AppType app_type =
418 proxy->AppRegistryCache().GetAppType(app_id);
419 if (app_type == apps::mojom::AppType::kUnknown)
420 continue;
421
422 if (app_type != apps::mojom::AppType::kExtension &&
423 app_type != apps::mojom::AppType::kWeb) {
424 return false;
425 }
426 }
427
428 // For Extension apps, and Web apps, AppServiceAppWindowLauncherController
429 // should only handle Chrome apps, managed by extensions::AppWindow, which
430 // should set |browser_context| in AppService InstanceRegistry. So if
431 // |browser_context| is not null, the app is a Chrome app,
432 // AppServiceAppWindowLauncherController should handle it, otherwise, it is
433 // opened in a browser, and AppServiceAppWindowLauncherController should skip
434 // them.
435 //
436 // The window could be teleported from the inactive user's profile to the
437 // current active user, so search all proxies.
438 for (auto* profile : controller_->GetProfileList()) {
439 content::BrowserContext* browser_context = nullptr;
440 auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
441 bool found = false;
442 proxy->InstanceRegistry().ForOneInstance(
443 window, [&browser_context, &found](const apps::InstanceUpdate& update) {
444 browser_context = update.BrowserContext();
445 found = true;
446 });
447 if (!found)
448 continue;
449 return (browser_context) ? false : true;
450 }
451 return true;
452 }
453
GetAppId(aura::Window * window) const454 std::string AppServiceInstanceRegistryHelper::GetAppId(
455 aura::Window* window) const {
456 for (auto* profile : controller_->GetProfileList()) {
457 auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
458 std::string app_id = proxy->InstanceRegistry().GetShelfId(window).app_id;
459 if (!app_id.empty())
460 return app_id;
461 }
462 return std::string();
463 }
464
GetAppId(content::WebContents * contents) const465 std::string AppServiceInstanceRegistryHelper::GetAppId(
466 content::WebContents* contents) const {
467 std::string app_id = launcher_controller_helper_->GetAppID(contents);
468 if (!app_id.empty())
469 return app_id;
470 return extension_misc::kChromeAppId;
471 }
472
GetWindow(content::WebContents * contents)473 aura::Window* AppServiceInstanceRegistryHelper::GetWindow(
474 content::WebContents* contents) {
475 std::string app_id = launcher_controller_helper_->GetAppID(contents);
476 aura::Window* window = contents->GetNativeView();
477
478 // If |app_id| is empty, it is a browser tab. Returns the toplevel window in
479 // this case.
480 if (app_id.empty())
481 window = window->GetToplevelWindow();
482 return window;
483 }
484
GetWindows(const std::string & app_id)485 std::set<aura::Window*> AppServiceInstanceRegistryHelper::GetWindows(
486 const std::string& app_id) {
487 std::set<aura::Window*> windows;
488 for (auto* profile : controller_->GetProfileList()) {
489 auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
490 auto w = proxy->InstanceRegistry().GetWindows(app_id);
491 windows = base::STLSetUnion<std::set<aura::Window*>>(windows, w);
492 }
493 return windows;
494 }
495
GetState(aura::Window * window) const496 apps::InstanceState AppServiceInstanceRegistryHelper::GetState(
497 aura::Window* window) const {
498 for (auto* profile : controller_->GetProfileList()) {
499 auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
500 auto state = proxy->InstanceRegistry().GetState(window);
501 if (state != apps::InstanceState::kUnknown)
502 return state;
503 }
504 return apps::InstanceState::kUnknown;
505 }
506
AddTabWindow(const std::string & app_id,aura::Window * window)507 void AppServiceInstanceRegistryHelper::AddTabWindow(const std::string& app_id,
508 aura::Window* window) {
509 if (app_id == extension_misc::kChromeAppId)
510 return;
511
512 aura::Window* top_level_window = window->GetToplevelWindow();
513 browser_window_to_tab_window_[top_level_window].insert(window);
514 tab_window_to_browser_window_[window] = top_level_window;
515 }
516
RemoveTabWindow(const std::string & app_id,aura::Window * window)517 void AppServiceInstanceRegistryHelper::RemoveTabWindow(
518 const std::string& app_id,
519 aura::Window* window) {
520 if (app_id == extension_misc::kChromeAppId)
521 return;
522
523 auto it = tab_window_to_browser_window_.find(window);
524 if (it == tab_window_to_browser_window_.end())
525 return;
526
527 aura::Window* top_level_window = it->second;
528
529 auto browser_it = browser_window_to_tab_window_.find(top_level_window);
530 DCHECK(browser_it != browser_window_to_tab_window_.end());
531 browser_it->second.erase(window);
532 if (browser_it->second.empty())
533 browser_window_to_tab_window_.erase(browser_it);
534 tab_window_to_browser_window_.erase(it);
535 }
536
UpdateTabWindow(const std::string & app_id,aura::Window * window)537 void AppServiceInstanceRegistryHelper::UpdateTabWindow(
538 const std::string& app_id,
539 aura::Window* window) {
540 RemoveTabWindow(app_id, window);
541 AddTabWindow(app_id, window);
542 }
543