blob: eb62dca8569d063a9a9b3baf008b1f4414f2cd13 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/apps/intent_helper/ash_intent_picker_helpers.h"
#include <algorithm>
#include <utility>
#include "ash/constants/ash_switches.h"
#include "base/bind.h"
#include "base/debug/dump_without_crashing.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/intent_helper/apps_navigation_types.h"
#include "chrome/browser/apps/intent_helper/intent_picker_auto_display_service.h"
#include "chrome/browser/apps/intent_helper/intent_picker_constants.h"
#include "chrome/browser/apps/intent_helper/intent_picker_internal.h"
#include "chrome/browser/ash/apps/metrics/intent_handling_metrics.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/intent_picker_tab_helper.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/common/chrome_features.h"
#include "components/services/app_service/public/mojom/types.mojom.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/models/image_model.h"
#include "ui/display/types/display_constants.h"
namespace apps {
namespace {
std::vector<IntentPickerAppInfo> FindAppsForUrl(
content::WebContents* web_contents,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
AppServiceProxyChromeOs* proxy =
AppServiceProxyFactory::GetForProfile(profile);
std::vector<std::string> app_ids =
proxy->GetAppIdsForUrl(url, /*exclude_browser=*/true);
for (const std::string& app_id : app_ids) {
proxy->AppRegistryCache().ForOneApp(
app_id, [&apps](const AppUpdate& update) {
apps.emplace(apps.begin(), GetPickerEntryType(update.AppType()),
ui::ImageModel(), update.AppId(), update.Name());
});
}
return apps;
}
bool ShouldAutoDisplayUi(
const std::vector<IntentPickerAppInfo>& apps_for_picker,
content::NavigationHandle* navigation_handle) {
content::WebContents* web_contents = navigation_handle->GetWebContents();
if (web_contents->GetVisibility() == content::Visibility::HIDDEN) {
return false;
}
const GURL& url = navigation_handle->GetURL();
if (apps_for_picker.empty())
return false;
if (InAppBrowser(web_contents))
return false;
if (!ShouldOverrideUrlLoading(GetStartingGURL(navigation_handle), url))
return false;
IntentPickerAutoDisplayService* ui_auto_display_service =
IntentPickerAutoDisplayService::Get(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
// On devices with tablet form factor we should not pop out the intent
// picker if Chrome has been chosen by the user as the platform for this URL.
if (chromeos::switches::IsTabletFormFactor()) {
if (ui_auto_display_service->GetLastUsedPlatformForTablets(url) ==
IntentPickerAutoDisplayPref::Platform::kChrome) {
return false;
}
}
// If we only have PWAs in the app list, do not show the intent picker.
// Instead just show the omnibox icon. This is to reduce annoyance to users
// until "Remember my choice" is available for desktop PWAs.
// TODO(crbug.com/826982): show the intent picker when the app registry is
// available to persist "Remember my choice" for PWAs.
if (!base::FeatureList::IsEnabled(features::kIntentPickerPWAPersistence) &&
ContainsOnlyPwasAndMacApps(apps_for_picker)) {
return false;
}
// If the preferred app is use browser, do not show the intent picker.
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
AppServiceProxyChromeOs* proxy =
AppServiceProxyFactory::GetForProfile(profile);
if (proxy) {
auto preferred_app_id = proxy->PreferredApps().FindPreferredAppForUrl(url);
if (preferred_app_id.has_value() &&
preferred_app_id.value() == apps::kUseBrowserForLink) {
return false;
}
}
return ui_auto_display_service->ShouldAutoDisplayUi(url);
}
PickerShowState GetPickerShowState(
content::NavigationHandle* navigation_handle,
const std::vector<IntentPickerAppInfo>& apps_for_picker) {
return ShouldAutoDisplayUi(apps_for_picker, navigation_handle) &&
IsNavigateFromLink(navigation_handle)
? PickerShowState::kPopOut
: PickerShowState::kOmnibox;
}
void OnIntentPickerClosed(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
const std::string& launch_name,
PickerEntryType entry_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
if (chromeos::switches::IsTabletFormFactor() && should_persist) {
// On devices of tablet form factor, until the user has decided to persist
// the setting, the browser-side intent picker should always be seen.
auto platform = IntentPickerAutoDisplayPref::Platform::kNone;
if (entry_type == PickerEntryType::kArc) {
platform = IntentPickerAutoDisplayPref::Platform::kArc;
} else if (entry_type == PickerEntryType::kUnknown &&
close_reason == IntentPickerCloseReason::STAY_IN_CHROME) {
platform = IntentPickerAutoDisplayPref::Platform::kChrome;
}
IntentPickerAutoDisplayService::Get(
Profile::FromBrowserContext(web_contents->GetBrowserContext()))
->UpdatePlatformForTablets(url, platform);
}
const bool should_launch_app =
close_reason == IntentPickerCloseReason::OPEN_APP;
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
AppServiceProxyChromeOs* proxy =
AppServiceProxyFactory::GetForProfile(profile);
// If the picker was closed without an app being chosen,
// e.g. due to the tab being closed. Keep count of this scenario so we can
// stop the UI from showing after 2+ dismissals.
if (entry_type == PickerEntryType::kUnknown &&
close_reason == IntentPickerCloseReason::DIALOG_DEACTIVATED) {
if (ui_auto_display_service) {
ui_auto_display_service->IncrementCounter(url);
}
}
if (should_persist) {
// TODO(https://crbug.com/853604): Remove this and convert to a DCHECK
// after finding out the root cause.
if (launch_name.empty()) {
base::debug::DumpWithoutCrashing();
} else {
proxy->AddPreferredApp(launch_name, url);
}
}
if (should_launch_app) {
if (entry_type == PickerEntryType::kWeb) {
web_app::ReparentWebContentsIntoAppBrowser(web_contents, launch_name);
} else {
// TODO(crbug.com/853604): Distinguish the source from link and omnibox.
mojom::LaunchSource launch_source = mojom::LaunchSource::kFromLink;
proxy->LaunchAppWithUrl(
launch_name,
GetEventFlags(mojom::LaunchContainer::kLaunchContainerWindow,
WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/true),
url, launch_source, apps::MakeWindowInfo(display::kDefaultDisplayId));
CloseOrGoBack(web_contents);
}
}
IntentHandlingMetrics::PickerAction action =
IntentHandlingMetrics::GetPickerAction(entry_type, close_reason,
should_persist);
IntentHandlingMetrics::Platform platform =
IntentHandlingMetrics::GetDestinationPlatform(launch_name, action);
IntentHandlingMetrics::RecordIntentPickerMetrics(
Source::kHttpOrHttps, should_persist, action, platform);
}
void OnAppIconsLoaded(content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
ShowIntentPickerBubbleForApps(
web_contents, std::move(apps),
/*show_stay_in_chrome=*/true,
/*show_remember_selection=*/true,
base::BindOnce(&OnIntentPickerClosed, web_contents,
ui_auto_display_service, url));
}
} // namespace
void MaybeShowIntentPickerAsh(content::NavigationHandle* navigation_handle) {
content::WebContents* web_contents = navigation_handle->GetWebContents();
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
return;
const GURL& url = navigation_handle->GetURL();
std::vector<IntentPickerAppInfo> apps = FindAppsForUrl(web_contents, url, {});
if (apps.empty()) {
IntentPickerTabHelper::SetShouldShowIcon(web_contents, false);
return;
}
IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
IntentPickerAutoDisplayService* ui_auto_display_service =
IntentPickerAutoDisplayService::Get(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
if (GetPickerShowState(navigation_handle, apps) ==
PickerShowState::kOmnibox) {
return;
}
IntentPickerTabHelper::LoadAppIcons(
web_contents, std::move(apps),
base::BindOnce(&OnAppIconsLoaded, web_contents, ui_auto_display_service,
url));
}
void ShowIntentPickerBubbleAsh(content::WebContents* web_contents,
const GURL& url) {
std::vector<IntentPickerAppInfo> apps = FindAppsForUrl(web_contents, url, {});
if (apps.empty())
return;
IntentPickerTabHelper::LoadAppIcons(
web_contents, std::move(apps),
base::BindOnce(&OnAppIconsLoaded, web_contents,
/*ui_auto_display_service=*/nullptr, url));
}
bool ContainsOnlyPwasAndMacApps(const std::vector<IntentPickerAppInfo>& apps) {
return std::all_of(apps.begin(), apps.end(),
[](const IntentPickerAppInfo& app_info) {
return app_info.type == PickerEntryType::kWeb ||
app_info.type == PickerEntryType::kMacOs;
});
}
PickerEntryType GetPickerEntryType(mojom::AppType app_type) {
PickerEntryType picker_entry_type = PickerEntryType::kUnknown;
switch (app_type) {
case mojom::AppType::kUnknown:
case mojom::AppType::kBuiltIn:
case mojom::AppType::kCrostini:
case mojom::AppType::kPluginVm:
case mojom::AppType::kExtension:
case mojom::AppType::kStandaloneBrowser:
case mojom::AppType::kRemote:
case mojom::AppType::kBorealis:
break;
case mojom::AppType::kArc:
picker_entry_type = PickerEntryType::kArc;
break;
case mojom::AppType::kWeb:
case mojom::AppType::kSystemWeb:
picker_entry_type = PickerEntryType::kWeb;
break;
case mojom::AppType::kMacOs:
picker_entry_type = PickerEntryType::kMacOs;
break;
}
return picker_entry_type;
}
} // namespace apps