blob: 47ae299bed1e3e8d583b45c6ab9108198415bf34 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/android/modal_dialog_wrapper.h"
#include "base/android/callback_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_callback.h"
#include "base/android/scoped_java_ref.h"
#include "base/functional/bind.h"
#include "ui/android/modal_dialog_manager_bridge.h"
#include "ui/android/window_android.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/strings/grit/ui_strings.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "ui/android/ui_android_jni_headers/ModalDialogWrapper_jni.h"
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace ui {
ModalDialogWrapper* g_dialog_ptr_for_testing = nullptr;
// static:
void ModalDialogWrapper::ShowTabModal(
std::unique_ptr<ui::DialogModel> dialog_model,
ui::WindowAndroid* window) {
ModalDialogWrapper* tab_modal =
new ModalDialogWrapper(std::move(dialog_model), window);
g_dialog_ptr_for_testing = tab_modal;
tab_modal->BuildPropertyModel();
auto* dialog_manager = window->GetModalDialogManagerBridge();
CHECK(dialog_manager);
dialog_manager->ShowDialog(tab_modal->java_obj_,
ModalDialogManagerBridge::ModalDialogType::kTab);
// `tab_modal` will delete itself when dialog is dismissed.
}
// static:
ModalDialogWrapper* ModalDialogWrapper::GetDialogForTesting() {
return g_dialog_ptr_for_testing;
}
// private:
ModalDialogWrapper::ModalDialogWrapper(
std::unique_ptr<DialogModel> dialog_model,
ui::WindowAndroid* window_android)
: dialog_model_(std::move(dialog_model)), window_android_(window_android) {
JNIEnv* env = base::android::AttachCurrentThread();
java_obj_ = Java_ModalDialogWrapper_create(
env, reinterpret_cast<uintptr_t>(this), window_android_->GetJavaObject());
}
ModalDialogWrapper::~ModalDialogWrapper() {
dialog_model_->OnDialogDestroying(DialogModelHost::GetPassKey());
}
namespace { // private helper for ModalDialogWrapper::BuildPropertyModel
ScopedJavaLocalRef<jstring> GetButtonLabel(JNIEnv* env,
DialogModel::Button* button,
int default_label_id) {
if (!button) {
return ScopedJavaLocalRef<jstring>();
}
const std::u16string& label_text = button->label();
return ConvertUTF16ToJavaString(
env, label_text.empty() ? l10n_util::GetStringUTF16(default_label_id)
: label_text);
}
std::u16string getMessageParagraph(DialogModelField* field) {
const DialogModelLabel& label = field->AsParagraph()->label();
std::u16string text;
auto replacements = label.replacements();
if (replacements.empty()) {
text = label.GetString();
} else {
std::vector<std::u16string> string_replacements;
for (auto replacement : replacements) {
string_replacements.push_back(replacement.text());
}
text = l10n_util::GetStringFUTF16(label.message_id(), string_replacements,
nullptr);
}
return text;
}
const SkBitmap* getIconBitmap(const ui::ImageModel& icon_model) {
auto key = ui::ColorProviderKey();
ui::ColorProvider* color_provider =
ui::ColorProviderManager::Get().GetColorProviderFor(key);
CHECK(color_provider);
gfx::ImageSkia image_skia = icon_model.Rasterize(color_provider);
// Returns the 1x Skia if it exists. See ImageSkia.bitmap() for details.
return image_skia.bitmap();
}
} // anonymous namespace
ModalDialogWrapper::ModalDialogButtonStyles
ModalDialogWrapper::GetButtonStyles() const {
auto* ok_button = dialog_model_->ok_button(DialogModelHost::GetPassKey());
if (!ok_button) {
return ModalDialogButtonStyles::kPrimaryOutlineNegativeOutline;
}
auto* cancel_button =
dialog_model_->cancel_button(DialogModelHost::GetPassKey());
const ButtonStyle ok_button_style =
ok_button->style().value_or(ButtonStyle::kDefault);
const ButtonStyle cancel_button_style =
cancel_button ? cancel_button->style().value_or(ui::ButtonStyle::kDefault)
: ButtonStyle::kDefault;
const std::optional<mojom::DialogButton>& override_default_button =
dialog_model_->override_default_button(DialogModelHost::GetPassKey());
const bool is_ok_prominent =
(override_default_button == mojom::DialogButton::kOk) ||
(ok_button_style == ui::ButtonStyle::kProminent &&
!override_default_button.has_value());
const bool is_cancel_prominent =
(override_default_button == mojom::DialogButton::kCancel) ||
(cancel_button_style == ui::ButtonStyle::kProminent &&
!override_default_button.has_value());
if (is_ok_prominent && is_cancel_prominent) {
NOTREACHED() << "Both buttons cannot be prominent.";
}
if (is_ok_prominent) {
return (cancel_button)
? ModalDialogButtonStyles::kPrimaryFilledNegativeOutline
: ModalDialogButtonStyles::kPrimaryFilledNoNegative;
}
if (is_cancel_prominent) {
return ModalDialogButtonStyles::kPrimaryOutlineNegativeFilled;
}
return ModalDialogButtonStyles::kPrimaryOutlineNegativeOutline;
}
void ModalDialogWrapper::BuildPropertyModel() {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jstring> title = ConvertUTF16ToJavaString(
env, dialog_model_->title(DialogModelHost::GetPassKey()));
ScopedJavaLocalRef<jstring> ok_button_label = GetButtonLabel(
env, dialog_model_->ok_button(DialogModelHost::GetPassKey()), IDS_APP_OK);
ScopedJavaLocalRef<jstring> cancel_button_label = GetButtonLabel(
env, dialog_model_->cancel_button(DialogModelHost::GetPassKey()),
IDS_APP_CANCEL);
ModalDialogButtonStyles buttonStyles = GetButtonStyles();
Java_ModalDialogWrapper_withTitleAndButtons(
env, java_obj_, title, ok_button_label, cancel_button_label,
(int)buttonStyles);
const SkBitmap* bitmap =
getIconBitmap(dialog_model_->icon(DialogModelHost::GetPassKey()));
if (!bitmap->isNull()) {
Java_ModalDialogWrapper_withTitleIcon(env, java_obj_,
gfx::ConvertToJavaBitmap(*bitmap));
}
std::u16string checkbox_text;
jboolean checked = false;
std::vector<std::u16string> paragraphs;
std::vector<const SkBitmap*> menu_item_icons;
std::vector<std::u16string> menu_item_labels;
menu_items_.clear();
for (const auto& field :
dialog_model_->fields(DialogModelHost::GetPassKey())) {
switch (field->type()) {
case DialogModelField::kParagraph: {
paragraphs.push_back(getMessageParagraph(field.get()));
break;
}
case DialogModelField::kCheckbox: {
// TODO(crbug.com/428048190): A dialog should not have more than one
// checkbox.
CHECK(checkbox_text.empty())
<< "Dialogs with more than one checkbox are "
"not yet supported on Android.";
DialogModelCheckbox* checkbox_field = field->AsCheckbox();
const DialogModelLabel& label = checkbox_field->label();
// Checkboxes with replacements (links) are not yet supported on
// Android.
CHECK(label.replacements().empty());
checkbox_text = label.GetString();
checked = checkbox_field->is_checked();
checkbox_id_ = checkbox_field->id();
break;
}
case DialogModelField::kMenuItem: {
DialogModelMenuItem* menu_item = field->AsMenuItem();
const SkBitmap* icon_bitmap = getIconBitmap(menu_item->icon());
// Menu items without icons are not yet handled on Android.
if (icon_bitmap && !icon_bitmap->isNull()) {
menu_item_icons.push_back(icon_bitmap);
menu_item_labels.push_back(menu_item->label());
menu_items_.push_back(menu_item);
}
break;
}
default:
NOTREACHED() << "Unsupported DialogModel field type " << field->type()
<< ". Support should be added before this dialog is used "
"in android";
}
}
if (paragraphs.size() > 0) {
ScopedJavaLocalRef<jobjectArray> java_paragraphs_array =
base::android::ToJavaArrayOfStrings(env, paragraphs);
Java_ModalDialogWrapper_withMessageParagraphs(env, java_obj_,
java_paragraphs_array);
}
if (!checkbox_text.empty()) {
ScopedJavaLocalRef<jstring> java_checkbox_label =
ConvertUTF16ToJavaString(env, checkbox_text);
Java_ModalDialogWrapper_withCheckbox(env, java_obj_, java_checkbox_label,
checked);
}
if (!menu_item_icons.empty()) {
ScopedJavaLocalRef<jclass> bitmap_class =
base::android::GetClass(env, "android/graphics/Bitmap");
ScopedJavaLocalRef<jobjectArray> java_icons_array(
env, env->NewObjectArray(menu_item_icons.size(), bitmap_class.obj(),
nullptr));
for (size_t i = 0; i < menu_item_icons.size(); ++i) {
env->SetObjectArrayElement(
java_icons_array.obj(), i,
gfx::ConvertToJavaBitmap(*menu_item_icons[i]).obj());
}
ScopedJavaLocalRef<jobjectArray> java_labels_array =
base::android::ToJavaArrayOfStrings(env, menu_item_labels);
Java_ModalDialogWrapper_withMenuItems(env, java_obj_, java_icons_array,
java_labels_array);
}
}
void ModalDialogWrapper::PositiveButtonClicked(JNIEnv* env) {
dialog_model_->OnDialogAcceptAction(DialogModelHost::GetPassKey());
}
void ModalDialogWrapper::NegativeButtonClicked(JNIEnv* env) {
dialog_model_->OnDialogCancelAction(DialogModelHost::GetPassKey());
}
void ModalDialogWrapper::CheckboxToggled(JNIEnv* env, jboolean is_checked) {
if (!checkbox_id_) {
return;
}
dialog_model_->GetCheckboxByUniqueId(checkbox_id_)
->OnChecked(DialogModelFieldHost::GetPassKey(),
static_cast<bool>(is_checked));
}
void ModalDialogWrapper::MenuItemClicked(JNIEnv* env, jint index) {
CHECK_GE(index, 0);
CHECK_LT(static_cast<size_t>(index), menu_items_.size());
menu_items_[index]->OnActivated(DialogModelFieldHost::GetPassKey(), 0);
}
void ModalDialogWrapper::Dismissed(JNIEnv* env) {
dialog_model_->OnDialogCloseAction(DialogModelHost::GetPassKey());
}
void ModalDialogWrapper::Destroy(JNIEnv* env) {
delete this;
}
void ModalDialogWrapper::Close() {
auto* dialog_manager = window_android_->GetModalDialogManagerBridge();
CHECK(dialog_manager) << "The destruction of the ModalDialogManager.java "
"should also destroy this dialog wrapper.";
dialog_manager->DismissDialog(java_obj_);
}
void ModalDialogWrapper::OnDialogButtonChanged() {}
} // namespace ui