blob: ace63b1b6b38e041304a3a00954a55d9a533292f [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/sharesheet/sharesheet_service.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.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/sharesheet/share_action.h"
#include "chrome/browser/sharesheet/sharesheet_types.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/browser/web_contents.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/types/display_constants.h"
#include "ui/views/view.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/sharesheet/cros_sharesheet_service_delegate.h"
#include "ui/chromeos/strings/grit/ui_chromeos_strings.h"
#else
#include "chrome/browser/sharesheet/sharesheet_service_delegate.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
namespace sharesheet {
namespace {
std::u16string& GetSelectedApp() {
static base::NoDestructor<std::u16string> selected_app;
return *selected_app;
}
} // namespace
SharesheetService::SharesheetService(Profile* profile)
: profile_(profile),
sharesheet_action_cache_(
std::make_unique<SharesheetActionCache>(profile_)),
app_service_proxy_(
apps::AppServiceProxyFactory::GetForProfile(profile_)) {}
SharesheetService::~SharesheetService() = default;
void SharesheetService::ShowBubble(content::WebContents* web_contents,
apps::mojom::IntentPtr intent,
SharesheetMetrics::LaunchSource source,
DeliveredCallback delivered_callback,
CloseCallback close_callback) {
ShowBubble(web_contents, std::move(intent),
/*contains_hosted_document=*/false, source,
std::move(delivered_callback), std::move(close_callback));
}
void SharesheetService::ShowBubble(content::WebContents* web_contents,
apps::mojom::IntentPtr intent,
bool contains_hosted_document,
SharesheetMetrics::LaunchSource source,
DeliveredCallback delivered_callback,
CloseCallback close_callback) {
DCHECK(intent->action == apps_util::kIntentActionSend ||
intent->action == apps_util::kIntentActionSendMultiple);
SharesheetMetrics::RecordSharesheetLaunchSource(source);
auto* sharesheet_service_delegate =
GetOrCreateDelegate(web_contents->GetTopLevelNativeWindow());
ShowBubbleWithDelegate(
sharesheet_service_delegate, std::move(intent), contains_hosted_document,
std::move(delivered_callback), std::move(close_callback));
}
void SharesheetService::CloseBubble(gfx::NativeWindow native_window,
SharesheetResult result) {
SharesheetServiceDelegate* delegate = GetDelegate(native_window);
if (delegate == nullptr)
return;
delegate->CloseBubble(result);
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void SharesheetService::ShowNearbyShareBubbleForArc(
gfx::NativeWindow native_window,
apps::mojom::IntentPtr intent,
SharesheetMetrics::LaunchSource source,
DeliveredCallback delivered_callback,
CloseCallback close_callback,
ActionCleanupCallback action_cleanup_callback) {
DCHECK(intent->action == apps_util::kIntentActionSend ||
intent->action == apps_util::kIntentActionSendMultiple);
ShareAction* share_action = sharesheet_action_cache_->GetActionFromName(
l10n_util::GetStringUTF16(IDS_NEARBY_SHARE_FEATURE_NAME));
if (!share_action || !share_action->ShouldShowAction(
intent, false /*contains_google_document=*/)) {
std::move(delivered_callback).Run(SharesheetResult::kCancel);
return;
}
share_action->SetActionCleanupCallbackForArc(
std::move(action_cleanup_callback));
SharesheetMetrics::RecordSharesheetLaunchSource(source);
auto* sharesheet_service_delegate = GetOrCreateDelegate(native_window);
sharesheet_service_delegate->ShowNearbyShareBubbleForArc(
std::move(intent), std::move(delivered_callback),
std::move(close_callback));
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Cleanup delegate when bubble closes.
void SharesheetService::OnBubbleClosed(gfx::NativeWindow native_window,
const std::u16string& active_action) {
auto iter = active_delegates_.begin();
while (iter != active_delegates_.end()) {
if ((*iter)->GetNativeWindow() == native_window) {
if (!active_action.empty()) {
ShareAction* share_action =
sharesheet_action_cache_->GetActionFromName(active_action);
if (share_action != nullptr)
share_action->OnClosing(iter->get());
}
active_delegates_.erase(iter);
break;
}
++iter;
}
}
void SharesheetService::OnTargetSelected(gfx::NativeWindow native_window,
const std::u16string& target_name,
const TargetType type,
apps::mojom::IntentPtr intent,
views::View* share_action_view) {
SharesheetServiceDelegate* delegate = GetDelegate(native_window);
if (delegate == nullptr)
return;
RecordUserActionMetrics(target_name);
if (type == TargetType::kAction) {
ShareAction* share_action =
sharesheet_action_cache_->GetActionFromName(target_name);
if (share_action == nullptr)
return;
delegate->OnActionLaunched();
share_action->LaunchAction(delegate, share_action_view, std::move(intent));
} else if (type == TargetType::kArcApp || type == TargetType::kWebApp) {
DCHECK(intent);
LaunchApp(target_name, std::move(intent));
delegate->CloseBubble(SharesheetResult::kSuccess);
}
}
bool SharesheetService::OnAcceleratorPressed(
const ui::Accelerator& accelerator,
const std::u16string& active_action) {
if (active_action.empty())
return false;
ShareAction* share_action =
sharesheet_action_cache_->GetActionFromName(active_action);
DCHECK(share_action);
return share_action == nullptr
? false
: share_action->OnAcceleratorPressed(accelerator);
}
SharesheetServiceDelegate* SharesheetService::GetOrCreateDelegate(
gfx::NativeWindow native_window) {
auto* delegate = GetDelegate(native_window);
if (delegate == nullptr) {
auto new_delegate =
#if BUILDFLAG(IS_CHROMEOS_ASH)
std::make_unique<ash::sharesheet::CrosSharesheetServiceDelegate>(
native_window, this);
#else
std::make_unique<SharesheetServiceDelegate>(native_window, this);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
delegate = new_delegate.get();
active_delegates_.push_back(std::move(new_delegate));
}
return delegate;
}
SharesheetServiceDelegate* SharesheetService::GetDelegate(
gfx::NativeWindow native_window) {
auto iter = active_delegates_.begin();
while (iter != active_delegates_.end()) {
if ((*iter)->GetNativeWindow() == native_window) {
return iter->get();
}
++iter;
}
return nullptr;
}
bool SharesheetService::HasShareTargets(const apps::mojom::IntentPtr& intent,
bool contains_hosted_document) {
std::vector<apps::IntentLaunchInfo> intent_launch_info =
app_service_proxy_->GetAppsForIntent(intent);
return sharesheet_action_cache_->HasVisibleActions(
intent, contains_hosted_document) ||
(!contains_hosted_document && !intent_launch_info.empty());
}
Profile* SharesheetService::GetProfile() {
return profile_;
}
const gfx::VectorIcon* SharesheetService::GetVectorIcon(
const std::u16string& display_name) {
return sharesheet_action_cache_->GetVectorIconFromName(display_name);
}
// static
void SharesheetService::SetSelectedAppForTesting(
const std::u16string& target_name) {
GetSelectedApp() = target_name;
}
void SharesheetService::LoadAppIcons(
std::vector<apps::IntentLaunchInfo> intent_launch_info,
std::vector<TargetInfo> targets,
size_t index,
SharesheetServiceIconLoaderCallback callback) {
if (index >= intent_launch_info.size()) {
std::move(callback).Run(std::move(targets));
return;
}
// Making a copy because we move |intent_launch_info| out below.
auto app_id = intent_launch_info[index].app_id;
auto app_type = app_service_proxy_->AppRegistryCache().GetAppType(app_id);
auto icon_type =
(base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon))
? apps::mojom::IconType::kStandard
: apps::mojom::IconType::kUncompressed;
constexpr bool allow_placeholder_icon = false;
app_service_proxy_->LoadIcon(
app_type, app_id, icon_type, kIconSize, allow_placeholder_icon,
base::BindOnce(&SharesheetService::OnIconLoaded,
weak_factory_.GetWeakPtr(), std::move(intent_launch_info),
std::move(targets), index, std::move(callback)));
}
void SharesheetService::LaunchApp(const std::u16string& target_name,
apps::mojom::IntentPtr intent) {
auto launch_source = apps::mojom::LaunchSource::kFromSharesheet;
app_service_proxy_->LaunchAppWithIntent(
base::UTF16ToUTF8(target_name),
apps::GetEventFlags(apps::mojom::LaunchContainer::kLaunchContainerWindow,
WindowOpenDisposition::NEW_WINDOW,
/*prefer_container=*/true),
std::move(intent), launch_source,
apps::MakeWindowInfo(display::kDefaultDisplayId));
}
void SharesheetService::OnIconLoaded(
std::vector<apps::IntentLaunchInfo> intent_launch_info,
std::vector<TargetInfo> targets,
size_t index,
SharesheetServiceIconLoaderCallback callback,
apps::mojom::IconValuePtr icon_value) {
const auto& launch_entry = intent_launch_info[index];
const auto& app_type =
app_service_proxy_->AppRegistryCache().GetAppType(launch_entry.app_id);
auto target_type = TargetType::kUnknown;
if (app_type == apps::mojom::AppType::kArc) {
target_type = TargetType::kArcApp;
} else if (app_type == apps::mojom::AppType::kWeb) {
target_type = TargetType::kWebApp;
}
app_service_proxy_->AppRegistryCache().ForOneApp(
launch_entry.app_id, [&launch_entry, &targets, &icon_value,
&target_type](const apps::AppUpdate& update) {
targets.emplace_back(target_type, icon_value->uncompressed,
base::UTF8ToUTF16(launch_entry.app_id),
base::UTF8ToUTF16(update.Name()),
base::UTF8ToUTF16(launch_entry.activity_label),
launch_entry.activity_name);
});
LoadAppIcons(std::move(intent_launch_info), std::move(targets), index + 1,
std::move(callback));
}
void SharesheetService::OnAppIconsLoaded(SharesheetServiceDelegate* delegate,
apps::mojom::IntentPtr intent,
DeliveredCallback delivered_callback,
CloseCallback close_callback,
std::vector<TargetInfo> targets) {
RecordTargetCountMetrics(targets);
RecordShareDataMetrics(intent);
// If SetSelectedAppForTesting() has been called, immediately launch the app.
const std::u16string selected_app = GetSelectedApp();
if (!selected_app.empty()) {
SharesheetResult result = SharesheetResult::kCancel;
auto iter = std::find_if(targets.begin(), targets.end(),
[selected_app](const auto& target) {
return (target.type == TargetType::kArcApp ||
target.type == TargetType::kWebApp) &&
target.launch_name == selected_app;
});
if (iter != targets.end()) {
LaunchApp(selected_app, std::move(intent));
result = SharesheetResult::kSuccess;
}
std::move(delivered_callback).Run(result);
delegate->OnBubbleClosed(/*active_action=*/std::u16string());
return;
}
delegate->ShowBubble(std::move(targets), std::move(intent),
std::move(delivered_callback),
std::move(close_callback));
}
void SharesheetService::ShowBubbleWithDelegate(
SharesheetServiceDelegate* delegate,
apps::mojom::IntentPtr intent,
bool contains_hosted_document,
DeliveredCallback delivered_callback,
CloseCallback close_callback) {
std::vector<TargetInfo> targets;
auto& actions = sharesheet_action_cache_->GetShareActions();
auto iter = actions.begin();
while (iter != actions.end()) {
if ((*iter)->ShouldShowAction(intent, contains_hosted_document)) {
targets.emplace_back(TargetType::kAction, absl::nullopt,
(*iter)->GetActionName(), (*iter)->GetActionName(),
absl::nullopt, absl::nullopt);
}
++iter;
}
std::vector<apps::IntentLaunchInfo> intent_launch_info =
contains_hosted_document ? std::vector<apps::IntentLaunchInfo>()
: app_service_proxy_->GetAppsForIntent(intent);
SharesheetMetrics::RecordSharesheetAppCount(intent_launch_info.size());
LoadAppIcons(
std::move(intent_launch_info), std::move(targets), 0,
base::BindOnce(&SharesheetService::OnAppIconsLoaded,
weak_factory_.GetWeakPtr(), delegate, std::move(intent),
std::move(delivered_callback), std::move(close_callback)));
}
void SharesheetService::RecordUserActionMetrics(
const std::u16string& target_name) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (target_name == l10n_util::GetStringUTF16(IDS_NEARBY_SHARE_FEATURE_NAME)) {
SharesheetMetrics::RecordSharesheetActionMetrics(
SharesheetMetrics::UserAction::kNearbyAction);
} else if (target_name ==
l10n_util::GetStringUTF16(IDS_FILE_BROWSER_SHARE_BUTTON_LABEL)) {
SharesheetMetrics::RecordSharesheetActionMetrics(
SharesheetMetrics::UserAction::kDriveAction);
} else {
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Should be an app if we reached here.
auto app_type = app_service_proxy_->AppRegistryCache().GetAppType(
base::UTF16ToUTF8(target_name));
switch (app_type) {
case apps::mojom::AppType::kArc:
SharesheetMetrics::RecordSharesheetActionMetrics(
SharesheetMetrics::UserAction::kArc);
break;
case apps::mojom::AppType::kWeb:
// TODO(crbug.com/1186533): Add a separate metrics for System Web Apps if
// needed.
case apps::mojom::AppType::kSystemWeb:
SharesheetMetrics::RecordSharesheetActionMetrics(
SharesheetMetrics::UserAction::kWeb);
break;
case apps::mojom::AppType::kBuiltIn:
case apps::mojom::AppType::kCrostini:
case apps::mojom::AppType::kExtension:
case apps::mojom::AppType::kMacOs:
case apps::mojom::AppType::kPluginVm:
case apps::mojom::AppType::kStandaloneBrowser:
case apps::mojom::AppType::kRemote:
case apps::mojom::AppType::kBorealis:
case apps::mojom::AppType::kStandaloneBrowserExtension:
case apps::mojom::AppType::kUnknown:
NOTREACHED();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
void SharesheetService::RecordTargetCountMetrics(
const std::vector<TargetInfo>& targets) {
int arc_app_count = 0;
int web_app_count = 0;
for (const auto& target : targets) {
switch (target.type) {
case TargetType::kArcApp:
++arc_app_count;
break;
case TargetType::kWebApp:
++web_app_count;
break;
case TargetType::kAction:
RecordShareActionMetrics(target.launch_name);
break;
case TargetType::kUnknown:
NOTREACHED();
}
}
SharesheetMetrics::RecordSharesheetArcAppCount(arc_app_count);
SharesheetMetrics::RecordSharesheetWebAppCount(web_app_count);
}
void SharesheetService::RecordShareActionMetrics(
const std::u16string& target_name) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (target_name == l10n_util::GetStringUTF16(IDS_NEARBY_SHARE_FEATURE_NAME)) {
SharesheetMetrics::RecordSharesheetShareAction(
SharesheetMetrics::UserAction::kNearbyAction);
} else if (target_name ==
l10n_util::GetStringUTF16(IDS_FILE_BROWSER_SHARE_BUTTON_LABEL)) {
SharesheetMetrics::RecordSharesheetShareAction(
SharesheetMetrics::UserAction::kDriveAction);
} else {
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
NOTREACHED();
#if BUILDFLAG(IS_CHROMEOS_ASH)
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
void SharesheetService::RecordShareDataMetrics(
const apps::mojom::IntentPtr& intent) {
// Record whether or not we're sharing a drive folder.
// If |intent| has a |drive_share_url| but does not contain |share_text|,
// it is a Drive Folder.
const bool is_drive_folder = intent->drive_share_url.has_value() &&
intent->drive_share_url.value().is_valid() &&
intent->share_text.value_or("").empty();
SharesheetMetrics::RecordSharesheetIsDriveFolder(is_drive_folder);
// Record file count.
const size_t file_count =
intent->files.has_value() ? intent->files->size() : 0;
SharesheetMetrics::RecordSharesheetFilesSharedCount(file_count);
}
} // namespace sharesheet