// Copyright 2022 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/ui/sharing_hub/sharing_hub_bubble_controller_chromeos_impl.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/share/share_metrics.h"
#include "chrome/browser/sharesheet/sharesheet_metrics.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/browser/web_contents.h"
#include "ui/views/controls/button/button.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/sharesheet/sharesheet_service.h"
#include "chrome/browser/sharesheet/sharesheet_service_factory.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chromeos/crosapi/mojom/app_service_types.mojom.h"
#include "chromeos/crosapi/mojom/sharesheet.mojom.h"
#include "chromeos/crosapi/mojom/sharesheet_mojom_traits.h"
#include "chromeos/lacros/lacros_service.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace sharing_hub {
namespace {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
crosapi::mojom::IntentPtr CreateCrosapiShareIntent(
const std::string& share_text,
const std::string& share_title) {
crosapi::mojom::IntentPtr intent = crosapi::mojom::Intent::New();
intent->action = apps_util::kIntentActionSend;
intent->mime_type = "text/plain";
intent->share_text = share_text;
if (!share_title.empty())
intent->share_title = share_title;
return intent;
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
// Result of the CrOS Sharesheet, i.e. whether the user selects a share target
// after opening the Sharesheet.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Keep in sync with
// SharingHubSharesheetResult in src/tools/metrics/histograms/enums.xml.
enum class SharingHubSharesheetResult {
SUCCESS = 0,
CANCELED = 1,
kMaxValue = CANCELED,
};
const char kSharesheetResult[] =
"Sharing.SharingHubDesktop.CrOSSharesheetResult";
SharingHubSharesheetResult GetSharesheetResultHistogram(
sharesheet::SharesheetResult result) {
switch (result) {
case sharesheet::SharesheetResult::kSuccess:
return SharingHubSharesheetResult::SUCCESS;
case sharesheet::SharesheetResult::kCancel:
case sharesheet::SharesheetResult::kErrorAlreadyOpen:
case sharesheet::SharesheetResult::kErrorWindowClosed:
return SharingHubSharesheetResult::CANCELED;
}
}
void LogCrOSSharesheetResult(sharesheet::SharesheetResult result) {
UMA_HISTOGRAM_ENUMERATION(kSharesheetResult,
GetSharesheetResultHistogram(result));
}
} // namespace
// static
SharingHubBubbleController*
SharingHubBubbleController::CreateOrGetFromWebContents(
content::WebContents* web_contents) {
SharingHubBubbleControllerChromeOsImpl::CreateForWebContents(web_contents);
SharingHubBubbleControllerChromeOsImpl* controller =
SharingHubBubbleControllerChromeOsImpl::FromWebContents(web_contents);
return controller;
}
SharingHubBubbleControllerChromeOsImpl::
~SharingHubBubbleControllerChromeOsImpl() {
if (bubble_showing_) {
bubble_showing_ = false;
// Close any remnant Sharesheet dialog.
CloseSharesheet();
// We must deselect the icon manually since the Sharesheet will not be able
// to invoke OnSharesheetClosed() at this point.
DeselectIcon();
}
}
void SharingHubBubbleControllerChromeOsImpl::HideBubble() {
if (bubble_showing_) {
CloseSharesheet();
}
}
void SharingHubBubbleControllerChromeOsImpl::ShowBubble(
share::ShareAttempt attempt) {
Browser* browser = chrome::FindBrowserWithTab(web_contents());
// Ignore subsequent calls to open the Sharesheet if it already is open. This
// is especially for the Nearby Share dialog, where clicking outside of it
// will not dismiss the dialog.
if (bubble_showing_)
return;
bubble_showing_ = true;
ShowSharesheet(browser->window()->GetSharingHubIconButton());
share::LogShareSourceDesktop(share::ShareSourceDesktop::kOmniboxSharingHub);
}
SharingHubBubbleView*
SharingHubBubbleControllerChromeOsImpl::sharing_hub_bubble_view() const {
return nullptr;
}
bool SharingHubBubbleControllerChromeOsImpl::ShouldOfferOmniboxIcon() {
return !GetProfile()->IsIncognitoProfile() && !GetProfile()->IsGuestSession();
}
Profile* SharingHubBubbleControllerChromeOsImpl::GetProfile() const {
return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
}
void SharingHubBubbleControllerChromeOsImpl::OnVisibilityChanged(
content::Visibility visibility) {
// Cancel the current share attempt if the user switches to a different tab in
// the window. Switching windows is permitted since a Sharesheet is tied to
// the native window.
if (bubble_showing_ && visibility == content::Visibility::HIDDEN) {
CloseSharesheet();
}
}
void SharingHubBubbleControllerChromeOsImpl::ShowSharesheet(
views::Button* highlighted_button) {
DCHECK(highlighted_button);
highlighted_button_tracker_.SetView(highlighted_button);
#if BUILDFLAG(IS_CHROMEOS_ASH)
ShowSharesheetAsh();
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
ShowSharesheetLacros();
#endif
// Save the window in order to close the Sharesheet if the tab is closed. This
// will return the incorrect window if called later.
parent_window_ = GetWebContents().GetTopLevelNativeWindow();
parent_window_tracker_ = views::NativeWindowTracker::Create(parent_window_);
}
void SharingHubBubbleControllerChromeOsImpl::CloseSharesheet() {
if (parent_window_ && !parent_window_tracker_->WasNativeWindowDestroyed()) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
CloseSharesheetAsh();
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
CloseSharesheetLacros();
#endif
}
// OnSharesheetClosed() is not guaranteed to be called by the
// SharesheetController (specifically for the case where this is invoked by
// our destructor). Hence, we must explicitly set this null here.
parent_window_ = nullptr;
parent_window_tracker_ = nullptr;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
sharesheet::SharesheetService*
SharingHubBubbleControllerChromeOsImpl::GetSharesheetService() {
Profile* const profile =
Profile::FromBrowserContext(GetWebContents().GetBrowserContext());
if (!profile)
return nullptr;
return sharesheet::SharesheetServiceFactory::GetForProfile(profile);
}
void SharingHubBubbleControllerChromeOsImpl::ShowSharesheetAsh() {
sharesheet::SharesheetService* sharesheet_service = GetSharesheetService();
if (!sharesheet_service)
return;
apps::IntentPtr intent = apps_util::MakeShareIntent(
GetWebContents().GetLastCommittedURL().spec(),
base::UTF16ToUTF8(GetWebContents().GetTitle()));
sharesheet_service->ShowBubble(
&GetWebContents(), std::move(intent),
sharesheet::LaunchSource::kOmniboxShare,
base::BindOnce(&SharingHubBubbleControllerChromeOsImpl::OnShareDelivered,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(
&SharingHubBubbleControllerChromeOsImpl::OnSharesheetClosed,
weak_ptr_factory_.GetWeakPtr()));
}
void SharingHubBubbleControllerChromeOsImpl::CloseSharesheetAsh() {
sharesheet::SharesheetService* sharesheet_service = GetSharesheetService();
if (!sharesheet_service)
return;
sharesheet::SharesheetController* sharesheet_controller =
sharesheet_service->GetSharesheetController(parent_window_);
if (!sharesheet_controller)
return;
sharesheet_controller->CloseBubble(sharesheet::SharesheetResult::kCancel);
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
void SharingHubBubbleControllerChromeOsImpl::ShowSharesheetLacros() {
auto* const service = chromeos::LacrosService::Get();
if (!service || !service->IsAvailable<crosapi::mojom::Sharesheet>())
return;
crosapi::mojom::IntentPtr intent =
CreateCrosapiShareIntent(GetWebContents().GetLastCommittedURL().spec(),
base::UTF16ToUTF8(GetWebContents().GetTitle()));
service->GetRemote<crosapi::mojom::Sharesheet>()->ShowBubbleWithOnClosed(
lacros_window_utility::GetRootWindowUniqueId(
GetWebContents().GetTopLevelNativeWindow()),
sharesheet::LaunchSource::kOmniboxShare, std::move(intent),
base::BindOnce(
&SharingHubBubbleControllerChromeOsImpl::OnSharesheetClosedLacros,
weak_ptr_factory_.GetWeakPtr()));
}
void SharingHubBubbleControllerChromeOsImpl::CloseSharesheetLacros() {
auto* const service = chromeos::LacrosService::Get();
if (!service || !service->IsAvailable<crosapi::mojom::Sharesheet>())
return;
service->GetRemote<crosapi::mojom::Sharesheet>()->CloseBubble(
lacros_window_utility::GetRootWindowUniqueId(parent_window_));
}
void SharingHubBubbleControllerChromeOsImpl::OnSharesheetClosedLacros() {
OnSharesheetClosed(views::Widget::ClosedReason::kUnspecified);
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
void SharingHubBubbleControllerChromeOsImpl::OnShareDelivered(
sharesheet::SharesheetResult result) {
LogCrOSSharesheetResult(result);
}
void SharingHubBubbleControllerChromeOsImpl::OnSharesheetClosed(
views::Widget::ClosedReason reason) {
bubble_showing_ = false;
parent_window_ = nullptr;
parent_window_tracker_ = nullptr;
// Deselect the omnibox icon now that the Sharesheet is closed.
DeselectIcon();
}
void SharingHubBubbleControllerChromeOsImpl::DeselectIcon() {
if (!highlighted_button_tracker_.view())
return;
views::Button* button =
views::Button::AsButton(highlighted_button_tracker_.view());
if (button)
button->SetHighlighted(false);
}
SharingHubBubbleControllerChromeOsImpl::SharingHubBubbleControllerChromeOsImpl(
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<SharingHubBubbleControllerChromeOsImpl>(
*web_contents) {}
WEB_CONTENTS_USER_DATA_KEY_IMPL(SharingHubBubbleControllerChromeOsImpl);
} // namespace sharing_hub