blob: 0ba22c063c06f2bd0d3c7e9a04e0d751d9673ec0 [file]
// Copyright 2025 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/android/extensions/extensions_toolbar_android.h"
#include <cstdint>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/android/extensions/extension_action_delegate_android.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/extensions/extensions_toolbar_view_model.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/permissions_manager.h"
#include "ui/base/unowned_user_data/scoped_unowned_user_data.h"
#include "ui/color/color_provider.h"
#include "ui/events/android/key_event_android.h"
#include "ui/gfx/android/java_bitmap.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/gfx/paint_vector_icon.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/ui/android/extensions/jni_headers/ExtensionAction_jni.h"
#include "chrome/browser/ui/android/extensions/jni_headers/ExtensionsMenuButtonState_jni.h"
#include "chrome/browser/ui/android/extensions/jni_headers/ExtensionsToolbarBridge_jni.h"
#include "chrome/browser/ui/android/extensions/jni_headers/RequestAccessButtonParams_jni.h"
using base::android::AttachCurrentThread;
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaArrayOfStrings;
using content::WebContents;
namespace extensions {
ExtensionsToolbarAndroid::ExtensionsToolbarAndroid(
BrowserWindowInterface* browser,
const base::android::JavaRef<jobject>& java_object)
: browser_(browser),
toolbar_view_model_(std::make_unique<ExtensionsToolbarViewModel>(
this,
browser,
ToolbarActionsModel::Get(browser_->GetProfile()))),
scoped_toolbar_view_model_user_data_(browser->GetUnownedUserDataHost(),
*toolbar_view_model_),
keybinding_registry_(std::make_unique<ExtensionKeybindingRegistryAndroid>(
browser_->GetProfile(),
TabListInterface::From(browser),
toolbar_view_model_.get())),
java_object_(java_object) {
toolbar_view_model_observation_.Observe(toolbar_view_model_.get());
if (toolbar_view_model_->AreActionsInitialized()) {
// We missed the observer call for initialization.
OnActionsInitialized();
}
}
ExtensionsToolbarAndroid::~ExtensionsToolbarAndroid() = default;
bool ExtensionsToolbarAndroid::HasActivePopup() {
return Java_ExtensionsToolbarBridge_hasActivePopup(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::TriggerPopup(
const ToolbarActionsModel::ActionId& action_id,
std::unique_ptr<ExtensionViewHost> host) {
Java_ExtensionsToolbarBridge_triggerPopup(
AttachCurrentThread(), java_object_, action_id,
reinterpret_cast<int64_t>(host.release()));
}
void ExtensionsToolbarAndroid::ShowContextMenu(
const ToolbarActionsModel::ActionId& action_id) {
Java_ExtensionsToolbarBridge_showContextMenu(AttachCurrentThread(),
java_object_, action_id);
}
std::unique_ptr<ExtensionActionViewModel>
ExtensionsToolbarAndroid::CreateActionViewModel(
const ToolbarActionsModel::ActionId& action_id,
ExtensionsContainer* extensions_container) {
return ExtensionActionViewModel::Create(
action_id, browser_,
std::make_unique<ExtensionActionDelegateAndroid>(browser_.get(),
action_id, this));
}
base::android::ScopedJavaLocalRef<jobject>
ExtensionsToolbarAndroid::GetRequestAccessButtonParams(
JNIEnv* env,
content::WebContents* web_contents) {
ExtensionsToolbarViewModel::RequestAccessButtonParams params =
toolbar_view_model_->GetRequestAccessButtonParams(web_contents);
return Java_RequestAccessButtonParams_Constructor(env, params.extension_ids,
params.tooltip_text);
}
void ExtensionsToolbarAndroid::OnRequestAccessButtonParamsChanged(
content::WebContents* web_contents) {
Java_ExtensionsToolbarBridge_onRequestAccessButtonParamsChanged(
AttachCurrentThread(), java_object_);
}
void ExtensionsToolbarAndroid::HideActivePopup() {
return Java_ExtensionsToolbarBridge_hideActivePopup(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::CloseExtensionsMenuIfOpen() {
Java_ExtensionsToolbarBridge_closeExtensionsMenuIfOpen(AttachCurrentThread(),
java_object_);
}
bool ExtensionsToolbarAndroid::CanShowToolbarActionPopupForAPICall(
const std::string& action_id) {
return Java_ExtensionsToolbarBridge_hasPoppedOutAction(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::ToggleExtensionsMenu() {
// On Android, the menu is tied to the extensions menu button within Java and
// therefore this method is not used.
NOTIMPLEMENTED();
}
void ExtensionsToolbarAndroid::ShowManageExtensionsIPH() {
Java_ExtensionsToolbarBridge_showManageExtensionsIPH(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::OnActionsInitialized() {
for (const auto& action_id : toolbar_view_model_->GetAllActionIds()) {
RegisterIconObserverForAction(action_id);
}
Java_ExtensionsToolbarBridge_onActionsInitialized(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::OnActionAdded(
const ToolbarActionsModel::ActionId& action_id) {
RegisterIconObserverForAction(action_id);
Java_ExtensionsToolbarBridge_onActionAdded(AttachCurrentThread(),
java_object_, action_id);
}
void ExtensionsToolbarAndroid::OnActionRemoved(
const ToolbarActionsModel::ActionId& action_id) {
icon_subscriptions_.erase(action_id);
Java_ExtensionsToolbarBridge_onActionRemoved(AttachCurrentThread(),
java_object_, action_id);
}
void ExtensionsToolbarAndroid::OnActionUpdated(
const ToolbarActionsModel::ActionId& action_id) {
Java_ExtensionsToolbarBridge_onActionUpdated(AttachCurrentThread(),
java_object_, action_id);
}
void ExtensionsToolbarAndroid::OnPinnedActionsChanged() {
Java_ExtensionsToolbarBridge_onPinnedActionsChanged(AttachCurrentThread(),
java_object_);
}
void ExtensionsToolbarAndroid::OnActiveWebContentsChanged(
bool /*is_same_document*/,
content::WebContents* web_contents) {
Java_ExtensionsToolbarBridge_onActiveWebContentsChanged(
AttachCurrentThread(), java_object_, web_contents->GetJavaWebContents());
}
void ExtensionsToolbarAndroid::OnToolbarControlStateUpdated() {
Java_ExtensionsToolbarBridge_onToolbarControlStateUpdated(
AttachCurrentThread(), java_object_);
}
void ExtensionsToolbarAndroid::Destroy(JNIEnv* env) {
delete this;
}
base::android::ScopedJavaLocalRef<jobject> ExtensionsToolbarAndroid::GetAction(
JNIEnv* env,
const ToolbarActionsModel::ActionId& action_id,
content::WebContents* web_contents) {
ToolbarActionViewModel* action =
toolbar_view_model_->GetActionModelForId(action_id);
auto hover_card_state = action->GetHoverCardState(web_contents);
ExtensionActionViewModel::HoverCardUiState ui_state =
action->GetHoverCardUiState(hover_card_state, web_contents);
std::optional<std::string> site_access_title;
if (ui_state.site_access_title.has_value()) {
site_access_title = base::UTF16ToUTF8(ui_state.site_access_title.value());
}
std::optional<std::string> site_access_description;
if (ui_state.site_access_description.has_value()) {
site_access_description =
base::UTF16ToUTF8(ui_state.site_access_description.value());
}
std::optional<std::string> policy_text;
if (ui_state.policy_text.has_value()) {
policy_text = base::UTF16ToUTF8(ui_state.policy_text.value());
}
base::android::ScopedJavaLocalRef<jobject> java_hover_card_state =
Java_HoverCardState_Constructor(
env, static_cast<int>(hover_card_state.site_access),
site_access_title, site_access_description,
static_cast<int>(hover_card_state.policy), policy_text);
return Java_ExtensionAction_Constructor(
env, action_id, base::UTF16ToUTF8(action->GetActionName()),
base::UTF16ToUTF8(action->GetActionTitle(web_contents)),
base::UTF16ToUTF8(action->GetAccessibleName(web_contents)),
java_hover_card_state);
}
base::android::ScopedJavaLocalRef<jobject> ExtensionsToolbarAndroid::GetIcon(
JNIEnv* env,
const ToolbarActionsModel::ActionId& action_id,
content::WebContents* web_contents,
int canvas_width_dp,
int canvas_height_dp,
float scale_factor) {
gfx::Size size(canvas_width_dp, canvas_height_dp);
ToolbarActionViewModel* action =
toolbar_view_model_->GetActionModelForId(action_id);
DCHECK(action);
ui::ImageModel model = action->GetIcon(web_contents, size);
if (model.IsEmpty() || !model.IsImage()) {
return nullptr;
}
gfx::ImageSkia image_skia = model.GetImage().AsImageSkia();
const gfx::ImageSkiaRep& rep = image_skia.GetRepresentation(scale_factor);
const SkBitmap& bitmap = rep.GetBitmap();
if (bitmap.isNull()) {
return nullptr;
}
return gfx::ConvertToJavaBitmap(bitmap);
}
std::vector<ToolbarActionsModel::ActionId>
ExtensionsToolbarAndroid::GetAllActionIds(JNIEnv* env) {
const auto& ids = toolbar_view_model_->GetAllActionIds();
return std::vector(ids.begin(), ids.end());
}
std::vector<ToolbarActionsModel::ActionId>
ExtensionsToolbarAndroid::GetPinnedActionIds(JNIEnv* env) {
const auto& ids = toolbar_view_model_->GetPinnedActionIds();
return std::vector(ids.begin(), ids.end());
}
namespace {
class MenuButtonIconSource : public gfx::CanvasImageSource {
public:
MenuButtonIconSource(const gfx::VectorIcon& icon,
int width,
int height,
SkColor color)
: gfx::CanvasImageSource(gfx::Size(width, height)),
icon_(icon),
color_(color) {}
MenuButtonIconSource(const MenuButtonIconSource&) = delete;
MenuButtonIconSource& operator=(const MenuButtonIconSource&) = delete;
~MenuButtonIconSource() override = default;
// gfx::CanvasImageSource:
void Draw(gfx::Canvas* canvas) override {
int icon_size = std::min(size().width(), size().height());
int x = (size().width() - icon_size) / 2;
int y = (size().height() - icon_size) / 2;
canvas->Translate(gfx::Vector2d(x, y));
gfx::PaintVectorIcon(canvas, *icon_, icon_size, color_);
}
private:
const raw_ref<const gfx::VectorIcon> icon_;
SkColor color_;
};
} // namespace
base::android::ScopedJavaLocalRef<jobject>
ExtensionsToolbarAndroid::GetMenuButtonState(JNIEnv* env,
content::WebContents* web_contents,
int canvas_width_dp,
int canvas_height_dp,
float scale_factor,
int color) {
auto state = toolbar_view_model_->GetButtonState(*web_contents);
std::u16string tooltip =
ExtensionsToolbarViewModel::GetToolbarButtonTooltipText(state);
std::u16string accessible_text =
ExtensionsToolbarViewModel::GetToolbarButtonAccessibleText(state);
const gfx::VectorIcon& icon =
ExtensionsToolbarViewModel::GetToolbarButtonIcon(state);
gfx::ImageSkia image_skia = gfx::ImageSkia(
std::make_unique<MenuButtonIconSource>(
icon, canvas_width_dp, canvas_height_dp, static_cast<SkColor>(color)),
gfx::Size(canvas_width_dp, canvas_height_dp));
const SkBitmap& bitmap =
image_skia.GetRepresentation(scale_factor).GetBitmap();
base::android::ScopedJavaLocalRef<jobject> java_bitmap;
if (!bitmap.isNull()) {
java_bitmap = gfx::ConvertToJavaBitmap(bitmap);
}
return Java_ExtensionsMenuButtonState_Constructor(
env, base::UTF16ToUTF8(tooltip), base::UTF16ToUTF8(accessible_text),
java_bitmap);
}
bool ExtensionsToolbarAndroid::HandleKeyDownEvent(
JNIEnv* env,
const ui::KeyEventAndroid& key_event) {
return keybinding_registry_->HandleKeyDownEvent(key_event);
}
bool ExtensionsToolbarAndroid::IsActionDraggable(
JNIEnv* env,
const ToolbarActionsModel::ActionId& action_id) {
return toolbar_view_model_->IsActionDraggable(action_id);
}
void ExtensionsToolbarAndroid::OnRequestAccessButtonClicked(
JNIEnv* env,
content::WebContents* web_contents) {
ExtensionsToolbarViewModel::RequestAccessButtonParams params =
toolbar_view_model_->GetRequestAccessButtonParams(web_contents);
toolbar_view_model_->GrantSiteAccess(web_contents, params.extension_ids);
}
void ExtensionsToolbarAndroid::ExecuteUserAction(
const ToolbarActionsModel::ActionId& action_id,
ToolbarActionViewModel::InvocationSource source) {
toolbar_view_model_->ExecuteUserAction(action_id, source);
}
void ExtensionsToolbarAndroid::RegisterIconObserverForAction(
const ToolbarActionsModel::ActionId& action_id) {
ToolbarActionViewModel* action_model =
toolbar_view_model_->GetActionModelForId(action_id);
DCHECK(action_model);
icon_subscriptions_.insert_or_assign(
action_id, action_model->RegisterIconUpdateObserver(base::BindRepeating(
&ExtensionsToolbarAndroid::OnActionIconUpdated,
base::Unretained(this), action_id)));
}
void ExtensionsToolbarAndroid::OnActionIconUpdated(
const ToolbarActionsModel::ActionId& action_id) {
Java_ExtensionsToolbarBridge_onActionUpdated(AttachCurrentThread(),
java_object_, action_id);
}
void ExtensionsToolbarAndroid::MovePinnedAction(
const ToolbarActionsModel::ActionId& action_id,
int target_index) {
toolbar_view_model_->MovePinnedAction(action_id, target_index);
}
static int64_t JNI_ExtensionsToolbarBridge_Init(
JNIEnv* env,
const base::android::JavaRef<jobject>& java_object,
int64_t j_browser_window_interface) {
BrowserWindowInterface* browser =
reinterpret_cast<BrowserWindowInterface*>(j_browser_window_interface);
return reinterpret_cast<int64_t>(
new ExtensionsToolbarAndroid(browser, java_object));
}
} // namespace extensions
DEFINE_JNI(ExtensionAction)
DEFINE_JNI(ExtensionsToolbarBridge)