chromium/chrome/browser/sharesheet/sharesheet_service.cc

// Copyright 2020 The Chromium Authors
// 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 <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.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/share_action.h"
#include "chrome/browser/sharesheet/sharesheet_service_delegator.h"
#include "chrome/browser/sharesheet/sharesheet_types.h"
#include "chrome/grit/generated_resources.h"
#include "components/drive/drive_api_util.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/icon_effects.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "content/public/browser/web_contents.h"
#include "ui/display/types/display_constants.h"
#include "ui/views/view.h"

namespace sharesheet {

namespace {

std::u16string& GetSelectedApp() {
  static base::NoDestructor<std::u16string> selected_app;

  return *selected_app;
}

gfx::NativeWindow GetNativeWindowFromWebContents(
    base::WeakPtr<content::WebContents> web_contents) {
  if (!web_contents) {
    return nullptr;
  }

  return web_contents->GetTopLevelNativeWindow();
}

bool HasHostedDocument(const apps::Intent& intent) {
  return base::ranges::any_of(
      intent.files, [](const apps::IntentFilePtr& file) {
        return drive::util::HasHostedDocumentExtension(
            base::FilePath(file->url.ExtractFileName()));
      });
}

}  // namespace

SharesheetService::SharesheetService(Profile* profile)
    : profile_(profile),
      share_action_cache_(std::make_unique<ShareActionCache>(profile_)),
      app_service_proxy_(
          apps::AppServiceProxyFactory::GetForProfile(profile_)) {}

SharesheetService::~SharesheetService() = default;

void SharesheetService::ShowBubble(content::WebContents* web_contents,
                                   apps::IntentPtr intent,
                                   LaunchSource source,
                                   DeliveredCallback delivered_callback,
                                   CloseCallback close_callback) {
  ShowBubble(std::move(intent), source,
             base::BindOnce(&GetNativeWindowFromWebContents,
                            web_contents->GetWeakPtr()),
             std::move(delivered_callback), std::move(close_callback));
}

void SharesheetService::ShowBubble(
    apps::IntentPtr intent,
    LaunchSource source,
    GetNativeWindowCallback get_native_window_callback,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback) {
  DCHECK(intent);
  DCHECK(intent->IsShareIntent());
  CHECK(delivered_callback);

  SharesheetMetrics::RecordSharesheetLaunchSource(source);
  PrepareToShowBubble(std::move(intent), std::move(get_native_window_callback),
                      std::move(delivered_callback), std::move(close_callback));
}

SharesheetController* SharesheetService::GetSharesheetController(
    gfx::NativeWindow native_window) {
  SharesheetServiceDelegator* delegator = GetDelegator(native_window);
  if (!delegator)
    return nullptr;
  return delegator->GetSharesheetController();
}

#if BUILDFLAG(IS_CHROMEOS_ASH)
void SharesheetService::ShowNearbyShareBubbleForArc(
    gfx::NativeWindow native_window,
    apps::IntentPtr intent,
    LaunchSource source,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback,
    ActionCleanupCallback cleanup_callback) {
  DCHECK(intent);
  DCHECK(intent->IsShareIntent());

  ShareAction* share_action =
      share_action_cache_->GetActionFromType(ShareActionType::kNearbyShare);
  if (!share_action || !share_action->ShouldShowAction(
                           intent, false /*contains_google_document=*/)) {
    std::move(delivered_callback).Run(SharesheetResult::kCancel);
    return;
  }
  share_action->SetActionCleanupCallbackForArc(std::move(cleanup_callback));
  SharesheetMetrics::RecordSharesheetLaunchSource(source);

  if (!native_window) {
    std::move(delivered_callback).Run(SharesheetResult::kErrorWindowClosed);
    return;
  }
  auto* sharesheet_service_delegator = GetOrCreateDelegator(native_window);
  sharesheet_service_delegator->ShowNearbyShareBubbleForArc(
      std::move(intent), std::move(delivered_callback),
      std::move(close_callback));
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

// Cleanup delegator when bubble closes.
void SharesheetService::OnBubbleClosed(
    gfx::NativeWindow native_window,
    const std::optional<ShareActionType>& share_action_type) {
  auto iter = active_delegators_.begin();
  while (iter != active_delegators_.end()) {
    if ((*iter)->GetNativeWindow() == native_window) {
      if (share_action_type) {
        ShareAction* share_action =
            share_action_cache_->GetActionFromType(share_action_type.value());
        if (share_action != nullptr)
          share_action->OnClosing(iter->get()->GetSharesheetController());
      }
      active_delegators_.erase(iter);
      break;
    }
    ++iter;
  }
}

void SharesheetService::OnTargetSelected(
    gfx::NativeWindow native_window,
    const TargetType type,
    const std::optional<ShareActionType>& share_action_type,
    const std::optional<std::u16string>& app_name,
    apps::IntentPtr intent,
    views::View* share_action_view) {
  SharesheetServiceDelegator* delegator = GetDelegator(native_window);
  if (!delegator)
    return;

  RecordUserActionMetrics(share_action_type, app_name);
  if (type == TargetType::kAction) {
    CHECK(share_action_type.has_value());
    ShareAction* share_action =
        share_action_cache_->GetActionFromType(share_action_type.value());
    if (share_action == nullptr)
      return;
    bool has_action_view = share_action->HasActionView();
    delegator->OnActionLaunched(has_action_view);
    share_action->LaunchAction(delegator->GetSharesheetController(),
                               (has_action_view ? share_action_view : nullptr),
                               std::move(intent));
  } else if (type == TargetType::kArcApp || type == TargetType::kWebApp) {
    CHECK(intent);
    CHECK(app_name.has_value());
    LaunchApp(app_name.value(), std::move(intent));
    delegator->CloseBubble(SharesheetResult::kSuccess);
  }
}

bool SharesheetService::OnAcceleratorPressed(
    const ui::Accelerator& accelerator,
    const ShareActionType share_action_type) {
  ShareAction* share_action =
      share_action_cache_->GetActionFromType(share_action_type);
  DCHECK(share_action);
  return share_action == nullptr
             ? false
             : share_action->OnAcceleratorPressed(accelerator);
}

bool SharesheetService::HasShareTargets(const apps::IntentPtr& intent) {
  bool contains_hosted_document = HasHostedDocument(*intent);
  std::vector<apps::IntentLaunchInfo> intent_launch_info =
      app_service_proxy_->GetAppsForIntent(intent);

  return share_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::optional<ShareActionType>& share_action_type) {
  if (!share_action_type.has_value()) {
    return nullptr;
  }
  return share_action_cache_->GetVectorIconFromType(share_action_type.value());
}

void SharesheetService::ShowBubbleForTesting(
    gfx::NativeWindow native_window,
    apps::IntentPtr intent,
    LaunchSource source,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback,
    int num_actions_to_add) {
  CHECK(views::Widget::GetWidgetForNativeWindow(native_window));
  SharesheetMetrics::RecordSharesheetLaunchSource(source);
  for (int i = 0; i < num_actions_to_add; ++i) {
    share_action_cache_->AddShareActionForTesting();  // IN-TEST
  }
  auto targets = GetActionsForIntent(intent);
  OnReadyToShowBubble(native_window, std::move(intent),
                      std::move(delivered_callback), std::move(close_callback),
                      std::move(targets));
}

SharesheetUiDelegate* SharesheetService::GetUiDelegateForTesting(
    gfx::NativeWindow native_window) {
  auto* delegator = GetDelegator(native_window);
  return delegator->GetUiDelegateForTesting();  // IN-TEST
}

// static
void SharesheetService::SetSelectedAppForTesting(
    const std::u16string& target_name) {
  GetSelectedApp() = target_name;
}

void SharesheetService::PrepareToShowBubble(
    apps::IntentPtr intent,
    GetNativeWindowCallback get_native_window_callback,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback) {
  bool contains_hosted_document = HasHostedDocument(*intent);
  auto targets = GetActionsForIntent(intent);

  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(), std::move(intent),
                     std::move(get_native_window_callback),
                     std::move(delivered_callback), std::move(close_callback)));
}

std::vector<TargetInfo> SharesheetService::GetActionsForIntent(
    const apps::IntentPtr& intent) {
  bool contains_hosted_document = HasHostedDocument(*intent);
  std::vector<TargetInfo> targets;
  auto& actions = share_action_cache_->GetShareActions();
  auto iter = actions.begin();
  while (iter != actions.end()) {
    if ((*iter)->ShouldShowAction(intent, contains_hosted_document)) {
      targets.emplace_back(/*type=*/TargetType::kAction, /*icon=*/std::nullopt,
                           /*launch_name=*/(*iter)->GetActionName(),
                           /*display_name=*/(*iter)->GetActionName(),
                           /*share_action_type=*/(*iter)->GetActionType(),
                           /*secondary_display_name=*/std::nullopt,
                           /*activity_name=*/std::nullopt,
                           /*is_dlp_blocked=*/false);
    }
    ++iter;
  }
  return targets;
}

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;
  uint32_t icon_effects = app_service_proxy_->GetIconEffects(app_id);
  if (intent_launch_info[index].is_dlp_blocked) {
    icon_effects |= apps::IconEffects::kBlocked;
  }
  app_service_proxy_->LoadIconWithIconEffects(
      app_id, icon_effects, apps::IconType::kStandard, kIconSize,
      /*allow_placeholder_icon=*/false,
      base::BindOnce(&SharesheetService::OnIconLoaded,
                     weak_factory_.GetWeakPtr(), std::move(intent_launch_info),
                     std::move(targets), index, std::move(callback)));
}

void SharesheetService::OnIconLoaded(
    std::vector<apps::IntentLaunchInfo> intent_launch_info,
    std::vector<TargetInfo> targets,
    size_t index,
    SharesheetServiceIconLoaderCallback callback,
    apps::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::AppType::kArc) {
    target_type = TargetType::kArcApp;
  } else if (app_type == apps::AppType::kWeb ||
             app_type == apps::AppType::kSystemWeb) {
    target_type = TargetType::kWebApp;
  }

  app_service_proxy_->AppRegistryCache().ForOneApp(
      launch_entry.app_id, [&launch_entry, &targets, &icon_value,
                            &target_type](const apps::AppUpdate& update) {
        gfx::ImageSkia image_skia =
            (icon_value && icon_value->icon_type == apps::IconType::kStandard)
                ? icon_value->uncompressed
                : gfx::ImageSkia();
        targets.emplace_back(
            /*type=*/target_type, /*icon=*/image_skia,
            /*launch_name=*/base::UTF8ToUTF16(launch_entry.app_id),
            /*display_name=*/base::UTF8ToUTF16(update.Name()),
            /*share_action_type=*/std::nullopt,
            /*secondary_display_name=*/
            base::UTF8ToUTF16(launch_entry.activity_label),
            /*activity_name=*/launch_entry.activity_name,
            /*is_dlp_blocked=*/launch_entry.is_dlp_blocked);
      });

  LoadAppIcons(std::move(intent_launch_info), std::move(targets), index + 1,
               std::move(callback));
}

void SharesheetService::OnAppIconsLoaded(
    apps::IntentPtr intent,
    GetNativeWindowCallback get_native_window_callback,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback,
    std::vector<TargetInfo> targets) {
  gfx::NativeWindow native_window = std::move(get_native_window_callback).Run();
  // Note that checking |native_window| is not sufficient: |widget| can be null
  // even when |native_window| is 'true': https://crbug.com/1375887#c11
  views::Widget* const widget =
      views::Widget::GetWidgetForNativeWindow(native_window);
  if (!widget) {
    LOG(WARNING) << "Window has been closed";
    std::move(delivered_callback).Run(SharesheetResult::kErrorWindowClosed);
    return;
  }

  OnReadyToShowBubble(native_window, std::move(intent),
                      std::move(delivered_callback), std::move(close_callback),
                      std::move(targets));
}

void SharesheetService::OnReadyToShowBubble(
    gfx::NativeWindow native_window,
    apps::IntentPtr intent,
    DeliveredCallback delivered_callback,
    CloseCallback close_callback,
    std::vector<TargetInfo> targets) {
  auto* delegator = GetOrCreateDelegator(native_window);

  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;
    if (base::ranges::any_of(targets, [selected_app](const auto& target) {
          return (target.type == TargetType::kArcApp ||
                  target.type == TargetType::kWebApp) &&
                 target.launch_name == selected_app;
        })) {
      LaunchApp(selected_app, std::move(intent));
      result = SharesheetResult::kSuccess;
    }

    std::move(delivered_callback).Run(result);
    delegator->OnBubbleClosed(/*share_action_type=*/std::nullopt);
    return;
  }

  delegator->ShowBubble(std::move(targets), std::move(intent),
                        std::move(delivered_callback),
                        std::move(close_callback));
}

void SharesheetService::LaunchApp(const std::u16string& target_name,
                                  apps::IntentPtr intent) {
  app_service_proxy_->LaunchAppWithIntent(
      base::UTF16ToUTF8(target_name),
      apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
                          /*prefer_container=*/true),
      std::move(intent), apps::LaunchSource::kFromSharesheet,
      std::make_unique<apps::WindowInfo>(display::kDefaultDisplayId),
      base::DoNothing());
}

SharesheetServiceDelegator* SharesheetService::GetOrCreateDelegator(
    gfx::NativeWindow native_window) {
  auto* delegator = GetDelegator(native_window);
  if (delegator == nullptr) {
    auto new_delegator =
        std::make_unique<SharesheetServiceDelegator>(native_window, this);
    delegator = new_delegator.get();
    active_delegators_.push_back(std::move(new_delegator));
  }
  return delegator;
}

SharesheetServiceDelegator* SharesheetService::GetDelegator(
    gfx::NativeWindow native_window) {
  auto iter = active_delegators_.begin();
  while (iter != active_delegators_.end()) {
    if ((*iter)->GetNativeWindow() == native_window) {
      return iter->get();
    }
    ++iter;
  }
  return nullptr;
}

void SharesheetService::RecordUserActionMetrics(
    const std::optional<ShareActionType>& share_action_type,
    const std::optional<std::u16string>& app_name) {
  // One of the two optional fields must be set.
  CHECK(share_action_type || app_name);

  if (share_action_type) {
    switch (share_action_type.value()) {
      case ShareActionType::kExample:
        // This is a test. Do nothing.
        return;
      case ShareActionType::kNearbyShare:
        SharesheetMetrics::RecordSharesheetActionMetrics(
            SharesheetMetrics::UserAction::kNearbyAction);
        return;
      case ShareActionType::kDriveShare:
        SharesheetMetrics::RecordSharesheetActionMetrics(
            SharesheetMetrics::UserAction::kDriveAction);
        return;
      case ShareActionType::kCopyToClipboardShare:
        SharesheetMetrics::RecordSharesheetActionMetrics(
            SharesheetMetrics::UserAction::kCopyAction);
        return;
      default:
        NOTREACHED();
    }
  }

  if (app_name) {
    // Should be an app if we reached here.
    auto app_type = app_service_proxy_->AppRegistryCache().GetAppType(
        base::UTF16ToUTF8(app_name.value()));
    switch (app_type) {
      case apps::AppType::kArc:
        SharesheetMetrics::RecordSharesheetActionMetrics(
            SharesheetMetrics::UserAction::kArc);
        return;
      case apps::AppType::kWeb:
      // TODO(crbug.com/40172532): Add a separate metrics for System Web Apps if
      // needed.
      case apps::AppType::kSystemWeb:
        SharesheetMetrics::RecordSharesheetActionMetrics(
            SharesheetMetrics::UserAction::kWeb);
        return;
      case apps::AppType::kBuiltIn:
      case apps::AppType::kCrostini:
      case apps::AppType::kChromeApp:
      case apps::AppType::kPluginVm:
      case apps::AppType::kStandaloneBrowser:
      case apps::AppType::kRemote:
      case apps::AppType::kBorealis:
      case apps::AppType::kBruschetta:
      case apps::AppType::kStandaloneBrowserChromeApp:
      case apps::AppType::kExtension:
      case apps::AppType::kStandaloneBrowserExtension:
      case apps::AppType::kUnknown:
        NOTREACHED();
    }
  }
}

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:
        break;
      case TargetType::kUnknown:
        NOTREACHED_IN_MIGRATION();
    }
  }
  SharesheetMetrics::RecordSharesheetArcAppCount(arc_app_count);
  SharesheetMetrics::RecordSharesheetWebAppCount(web_app_count);
}

void SharesheetService::RecordShareDataMetrics(const apps::IntentPtr& intent) {
  // Record file count.
  SharesheetMetrics::RecordSharesheetFilesSharedCount(intent->files.size());
}

}  // namespace sharesheet