// 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 "components/webapps/browser/android/bottomsheet/pwa_bottom_sheet_controller.h"
#include <string>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/url_formatter/elide_url.h"
#include "components/webapps/browser/android/app_banner_manager_android.h"
#include "components/webapps/browser/banners/install_banner_config.h"
#include "components/webapps/browser/installable/installable_data.h"
#include "components/webapps/browser/webapps_client.h"
#include "components/webapps/common/constants.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/gfx/text_elider.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/webapps/browser/android/webapps_jni_headers/PwaBottomSheetControllerProvider_jni.h"
#include "components/webapps/browser/android/webapps_jni_headers/PwaBottomSheetController_jni.h"
using base::ASCIIToUTF16;
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace {
bool CanShowBottomSheet(content::WebContents* web_contents,
const std::vector<webapps::Screenshot>& screenshots) {
if (screenshots.size() == 0)
return false;
JNIEnv* env = base::android::AttachCurrentThread();
return Java_PwaBottomSheetControllerProvider_canShowPwaBottomSheetInstaller(
env, web_contents->GetJavaWebContents());
}
} // anonymous namespace
namespace webapps {
PwaBottomSheetController::~PwaBottomSheetController() = default;
// static
jboolean JNI_PwaBottomSheetController_RequestOrExpandBottomSheetInstaller(
JNIEnv* env,
const JavaParamRef<jobject>& jweb_contents,
int install_trigger) {
content::WebContents* web_contents =
content::WebContents::FromJavaWebContents(jweb_contents);
auto* app_banner_manager = static_cast<AppBannerManagerAndroid*>(
WebappsClient::Get()->GetAppBannerManager(web_contents));
std::optional<InstallBannerConfig> install_config =
app_banner_manager->GetCurrentBannerConfig();
if (!install_config.has_value()) {
return false;
}
WebappInstallSource install_source = InstallableMetrics::GetInstallSource(
web_contents, static_cast<InstallTrigger>(install_trigger));
return app_banner_manager->MaybeShowPwaBottomSheetController(
/* expand_sheet= */ true, install_source, install_config.value());
}
// static
bool PwaBottomSheetController::MaybeShow(
content::WebContents* web_contents,
const WebAppBannerData& web_app_banner_data,
bool expand_sheet,
base::RepeatingCallback<void(AddToHomescreenInstaller::Event,
const AddToHomescreenParams&)>
a2hs_event_callback,
std::unique_ptr<AddToHomescreenParams> a2hs_params) {
if (!CanShowBottomSheet(web_contents, web_app_banner_data.screenshots)) {
return false;
}
JNIEnv* env = base::android::AttachCurrentThread();
if (Java_PwaBottomSheetControllerProvider_doesBottomSheetExist(
env, web_contents->GetJavaWebContents())) {
Java_PwaBottomSheetControllerProvider_updateState(
env, web_contents->GetJavaWebContents(),
jint(a2hs_params->install_source), expand_sheet);
} else {
// Lifetime of this object is managed by the Java counterpart, iff bottom
// sheets can be shown (otherwise an infobar is used and this class is no
// longer needed).
PwaBottomSheetController* controller = new PwaBottomSheetController(
web_app_banner_data, std::move(a2hs_params),
std::move(a2hs_event_callback));
controller->ShowBottomSheetInstaller(web_contents, expand_sheet);
}
return true;
}
PwaBottomSheetController::PwaBottomSheetController(
const WebAppBannerData& web_app_banner_data,
std::unique_ptr<AddToHomescreenParams> a2hs_params,
base::RepeatingCallback<void(AddToHomescreenInstaller::Event,
const AddToHomescreenParams&)>
a2hs_event_callback)
: web_app_banner_data_(web_app_banner_data),
a2hs_params_(std::move(a2hs_params)),
a2hs_event_callback_(a2hs_event_callback) {}
void PwaBottomSheetController::Destroy(JNIEnv* env) {
// When the bottom sheet hasn't been expanded, it is considered equivalent to
// the regular install infobar and the expanded state equivalent
// to the regular install dialog prompt. Therefore, we send UI_CANCELLED
// only if the bottom sheet was ever expanded and not closed.
if (!install_triggered_ && !sheet_closed_ && sheet_expanded_) {
a2hs_event_callback_.Run(AddToHomescreenInstaller::Event::UI_CANCELLED,
*a2hs_params_);
}
delete this;
}
void PwaBottomSheetController::UpdateInstallSource(JNIEnv* env,
int install_source) {
a2hs_params_->install_source =
static_cast<WebappInstallSource>(install_source);
}
void PwaBottomSheetController::OnSheetClosedWithSwipe(JNIEnv* env) {
a2hs_event_callback_.Run(AddToHomescreenInstaller::Event::UI_CANCELLED,
*a2hs_params_);
sheet_closed_ = true;
}
void PwaBottomSheetController::OnSheetExpanded(JNIEnv* env) {
a2hs_event_callback_.Run(AddToHomescreenInstaller::Event::UI_SHOWN,
*a2hs_params_);
sheet_expanded_ = true;
}
void PwaBottomSheetController::OnAddToHomescreen(
JNIEnv* env,
const JavaParamRef<jobject>& jweb_contents) {
content::WebContents* web_contents =
content::WebContents::FromJavaWebContents(jweb_contents);
if (!web_contents)
return;
install_triggered_ = true;
AddToHomescreenInstaller::Install(web_contents, *a2hs_params_,
std::move(a2hs_event_callback_));
PwaInstallPathTracker::TrackInstallPath(/* bottom_sheet= */ true,
a2hs_params_->install_source);
}
void PwaBottomSheetController::ShowBottomSheetInstaller(
content::WebContents* web_contents,
bool expand_sheet) {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jstring> j_user_title =
ConvertUTF16ToJavaString(env, web_app_banner_data_.GetAppName());
// Trim down the app URL to the origin. Elide cryptographic schemes so HTTP
// is still shown.
ScopedJavaLocalRef<jstring> j_url = ConvertUTF16ToJavaString(
env, url_formatter::FormatUrlForSecurityDisplay(
web_app_banner_data_.manifest().start_url,
url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC));
std::u16string elided_description = gfx::TruncateString(
web_app_banner_data_.manifest().description.value_or(u""),
webapps::kMaximumDescriptionLength, gfx::CHARACTER_BREAK);
ScopedJavaLocalRef<jstring> j_description =
ConvertUTF16ToJavaString(env, elided_description);
ScopedJavaLocalRef<jobject> j_bitmap =
gfx::ConvertToJavaBitmap(web_app_banner_data_.primary_icon);
Java_PwaBottomSheetControllerProvider_showPwaBottomSheetInstaller(
env, reinterpret_cast<intptr_t>(this), web_contents->GetJavaWebContents(),
j_bitmap, web_app_banner_data_.has_maskable_primary_icon, j_user_title,
j_url, j_description);
for (const auto& screenshot : web_app_banner_data_.screenshots) {
if (!screenshot.image.isNull())
UpdateScreenshot(screenshot.image, web_contents);
}
if (expand_sheet) {
Java_PwaBottomSheetControllerProvider_expandPwaBottomSheetInstaller(
env, web_contents->GetJavaWebContents());
}
}
void PwaBottomSheetController::UpdateScreenshot(
const SkBitmap& screenshot,
content::WebContents* web_contents) {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> java_screenshot =
gfx::ConvertToJavaBitmap(screenshot);
// TODO(crbug.com/40870351): support passing label to use as
// the accessibility string.
Java_PwaBottomSheetController_addWebAppScreenshot(
env, java_screenshot, web_contents->GetJavaWebContents());
}
} // namespace webapps