| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/android/selection/selection_popup_controller.h" |
| |
| #include <cstdlib> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/android/scoped_java_ref.h" |
| #include "base/feature_list.h" |
| #include "content/browser/android/selection/composited_touch_handle_drawable.h" |
| #include "content/browser/gpu/gpu_data_manager_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_view_android.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/web_contents/web_contents_view_android.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/context_menu_params.h" |
| #include "content/public/common/content_features.h" |
| #include "third_party/blink/public/common/context_menu_data/edit_flags.h" |
| #include "third_party/blink/public/mojom/context_menu/context_menu.mojom.h" |
| #include "third_party/blink/public/mojom/input/input_handler.mojom.h" |
| #include "ui/base/mojom/menu_source_type.mojom.h" |
| #include "ui/gfx/android/android_surface_control_compat.h" |
| #include "ui/gfx/geometry/point_conversions.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "content/public/android/content_jni_headers/SelectionPopupControllerImpl_jni.h" |
| |
| using base::android::AttachCurrentThread; |
| using base::android::ConvertUTF16ToJavaString; |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::JavaParamRef; |
| using base::android::ScopedJavaLocalRef; |
| |
| namespace content { |
| namespace { |
| |
| const int kMaxOffsetAdjust = 50; |
| const int kMaxOffsetExtendedAdjust = 250; |
| |
| bool IsOffsetAdjustValid( |
| int startOffset, |
| int endOffset, |
| int surroundingTextLength, |
| const blink::mojom::SelectAroundCaretResultPtr& result) { |
| return std::abs(result->word_start_adjust) < kMaxOffsetAdjust && |
| std::abs(result->word_end_adjust) < kMaxOffsetAdjust && |
| std::abs(result->extended_start_adjust) < kMaxOffsetExtendedAdjust && |
| std::abs(result->extended_end_adjust) < kMaxOffsetExtendedAdjust && |
| startOffset + result->extended_start_adjust >= 0 && |
| startOffset + result->extended_start_adjust <= surroundingTextLength && |
| endOffset + result->extended_end_adjust >= 0 && |
| endOffset + result->extended_end_adjust <= surroundingTextLength; |
| } |
| |
| } // namespace |
| |
| namespace { |
| |
| bool IsAndroidSurfaceControlMagnifierEnabled() { |
| static bool enabled = gfx::SurfaceControl::SupportsSurfacelessControl(); |
| return enabled; |
| } |
| |
| BASE_FEATURE(DismissMagnifierOnViewSwap, |
| base::FEATURE_ENABLED_BY_DEFAULT); |
| |
| } // namespace |
| |
| static jboolean |
| JNI_SelectionPopupControllerImpl_IsMagnifierWithSurfaceControlSupported( |
| JNIEnv* env) { |
| GpuDataManagerImpl* manager = GpuDataManagerImpl::GetInstance(); |
| return manager->IsGpuFeatureInfoAvailable() && |
| manager->GetFeatureStatus( |
| gpu::GpuFeatureType::GPU_FEATURE_TYPE_ANDROID_SURFACE_CONTROL) == |
| gpu::kGpuFeatureStatusEnabled && |
| IsAndroidSurfaceControlMagnifierEnabled(); |
| } |
| |
| jlong JNI_SelectionPopupControllerImpl_Init( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| const JavaParamRef<jobject>& jweb_contents) { |
| WebContents* web_contents = WebContents::FromJavaWebContents(jweb_contents); |
| DCHECK(web_contents); |
| |
| // Owns itself and gets destroyed when |WebContentsDestroyed| is called. |
| auto* controller = new SelectionPopupController(env, obj, web_contents); |
| controller->Initialize(); |
| return reinterpret_cast<intptr_t>(controller); |
| } |
| |
| SelectionPopupController* SelectionPopupController::FromWebContents( |
| WebContents& web_contents) { |
| ScopedJavaLocalRef<jobject> jweb_contents = web_contents.GetJavaWebContents(); |
| JNIEnv* env = AttachCurrentThread(); |
| // Call the Java-side fromWebContents method. This gets the Java |
| // SelectionPopupController if it already exists. Otherwise, this will |
| // instantiate the Java SelectionPopupController and store it in the web |
| // contents. |
| ScopedJavaLocalRef<jobject> jselection_popup_controller = |
| Java_SelectionPopupControllerImpl_fromWebContents(env, jweb_contents); |
| // Then get the native pointer from the newly-created |
| // SelectionPopupController. The Java SelectionPopupController owns the C++ |
| // SelectionPopupController. |
| jlong selection_popup_controller = |
| Java_SelectionPopupControllerImpl_getNativePtr( |
| env, jselection_popup_controller); |
| return reinterpret_cast<SelectionPopupController*>( |
| selection_popup_controller); |
| } |
| |
| SelectionPopupController::SelectionPopupController( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| WebContents* web_contents) |
| : RenderWidgetHostConnector(web_contents) { |
| java_obj_ = JavaObjectWeakGlobalRef(env, obj); |
| } |
| |
| SelectionPopupController::~SelectionPopupController() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (!obj.is_null()) { |
| Java_SelectionPopupControllerImpl_nativeSelectionPopupControllerDestroyed( |
| env, obj); |
| } |
| } |
| |
| ScopedJavaLocalRef<jobject> SelectionPopupController::GetContext() const { |
| JNIEnv* env = AttachCurrentThread(); |
| |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return nullptr; |
| |
| return Java_SelectionPopupControllerImpl_getContext(env, obj); |
| } |
| |
| void SelectionPopupController::SetTextHandlesHiddenForDropdownMenu( |
| JNIEnv* env, |
| jboolean hidden) { |
| if (rwhva_) { |
| rwhva_->SetTextHandlesHiddenForDropdownMenu(hidden); |
| } |
| } |
| |
| void SelectionPopupController::SetTextHandlesTemporarilyHidden( |
| JNIEnv* env, |
| jboolean hidden) { |
| if (rwhva_) |
| rwhva_->SetTextHandlesTemporarilyHidden(hidden); |
| } |
| |
| ScopedJavaLocalRef<jobjectArray> SelectionPopupController::GetTouchHandleRects( |
| JNIEnv* env) { |
| if (!rwhva_ || !rwhva_->touch_selection_controller()) { |
| return nullptr; |
| } |
| gfx::RectF start_handle = |
| rwhva_->touch_selection_controller()->GetStartHandleRect(); |
| gfx::RectF end_handle = |
| rwhva_->touch_selection_controller()->GetEndHandleRect(); |
| std::vector<ScopedJavaLocalRef<jobject>> handle_rects; |
| ScopedJavaLocalRef<jobject> start = ScopedJavaLocalRef<jobject>( |
| Java_SelectionPopupControllerImpl_createJavaRect( |
| env, start_handle.x(), start_handle.y(), start_handle.right(), |
| start_handle.bottom())); |
| ScopedJavaLocalRef<jobject> end = ScopedJavaLocalRef<jobject>( |
| Java_SelectionPopupControllerImpl_createJavaRect( |
| env, end_handle.x(), end_handle.y(), end_handle.right(), |
| end_handle.bottom())); |
| handle_rects.push_back(start); |
| handle_rects.push_back(end); |
| return base::android::ToJavaArrayOfObjects(env, handle_rects); |
| } |
| |
| std::unique_ptr<ui::TouchHandleDrawable> |
| SelectionPopupController::CreateTouchHandleDrawable( |
| gfx::NativeView parent_native_view, |
| cc::slim::Layer* parent_layer) { |
| ScopedJavaLocalRef<jobject> activityContext = GetContext(); |
| // If activityContext is null then Application context is used instead on |
| // the java side in CompositedTouchHandleDrawable. |
| return std::make_unique<CompositedTouchHandleDrawable>( |
| parent_native_view, parent_layer, activityContext); |
| } |
| |
| void SelectionPopupController::MoveRangeSelectionExtent( |
| const gfx::PointF& extent) { |
| auto* web_contents_impl = static_cast<WebContentsImpl*>(web_contents()); |
| if (!web_contents_impl) |
| return; |
| |
| web_contents_impl->MoveRangeSelectionExtent(gfx::ToRoundedPoint(extent)); |
| } |
| |
| void SelectionPopupController::SelectBetweenCoordinates( |
| const gfx::PointF& base, |
| const gfx::PointF& extent) { |
| auto* web_contents_impl = static_cast<WebContentsImpl*>(web_contents()); |
| if (!web_contents_impl) |
| return; |
| |
| gfx::Point base_point = gfx::ToRoundedPoint(base); |
| gfx::Point extent_point = gfx::ToRoundedPoint(extent); |
| if (base_point == extent_point) |
| return; |
| |
| web_contents_impl->SelectRange(base_point, extent_point); |
| } |
| |
| void SelectionPopupController::UpdateRenderProcessConnection( |
| RenderWidgetHostViewAndroid* old_rwhva, |
| RenderWidgetHostViewAndroid* new_rwhva) { |
| if (old_rwhva) |
| old_rwhva->set_selection_popup_controller(nullptr); |
| if (new_rwhva) |
| new_rwhva->set_selection_popup_controller(this); |
| rwhva_ = new_rwhva; |
| |
| if (!base::FeatureList::IsEnabled(kDismissMagnifierOnViewSwap)) { |
| return; |
| } |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) { |
| return; |
| } |
| |
| Java_SelectionPopupControllerImpl_renderWidgetHostViewChanged(env, obj); |
| } |
| |
| void SelectionPopupController::OnSelectionEvent( |
| ui::SelectionEventType event, |
| const gfx::RectF& selection_rect) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return; |
| |
| Java_SelectionPopupControllerImpl_onSelectionEvent( |
| env, obj, event, selection_rect.x(), selection_rect.y(), |
| selection_rect.right(), selection_rect.bottom()); |
| } |
| |
| void SelectionPopupController::OnDragUpdate( |
| const ui::TouchSelectionDraggable::Type type, |
| const gfx::PointF& position) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return; |
| |
| Java_SelectionPopupControllerImpl_onDragUpdate( |
| env, obj, static_cast<int>(type), position.x(), position.y()); |
| } |
| |
| void SelectionPopupController::OnSelectionChanged(const std::string& text) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return; |
| ScopedJavaLocalRef<jstring> jtext = ConvertUTF8ToJavaString(env, text); |
| Java_SelectionPopupControllerImpl_onSelectionChanged(env, obj, jtext); |
| } |
| |
| bool SelectionPopupController::ShowSelectionMenu( |
| RenderFrameHost* render_frame_host, |
| const ContextMenuParams& params, |
| int handle_height) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return false; |
| |
| // Display paste pop-up only when selection is empty and editable. |
| const bool from_touch = |
| params.source_type == ui::mojom::MenuSourceType::kTouch || |
| params.source_type == ui::mojom::MenuSourceType::kLongPress || |
| params.source_type == ui::mojom::MenuSourceType::kTouchHandle || |
| params.source_type == ui::mojom::MenuSourceType::kStylus; |
| |
| const bool from_mouse = |
| params.source_type == ui::mojom::MenuSourceType::kMouse; |
| |
| const bool from_selection_adjustment = |
| params.source_type == ui::mojom::MenuSourceType::kAdjustSelection || |
| params.source_type == ui::mojom::MenuSourceType::kAdjustSelectionReset; |
| |
| // If source_type is not in the list then return. |
| if (!from_touch && !from_mouse && !from_selection_adjustment) |
| return false; |
| |
| // Don't show paste pop-up for non-editable textarea. |
| if (!params.is_editable && params.selection_text.empty()) { |
| return false; |
| } |
| |
| const bool can_select_all = |
| !!(params.edit_flags & blink::ContextMenuDataEditFlags::kCanSelectAll); |
| const bool can_edit_richly = |
| !!(params.edit_flags & blink::ContextMenuDataEditFlags::kCanEditRichly); |
| const bool is_password_type = |
| params.form_control_type == blink::mojom::FormControlType::kInputPassword; |
| const ScopedJavaLocalRef<jstring> jselected_text = |
| ConvertUTF16ToJavaString(env, params.selection_text); |
| const bool should_suggest = |
| params.source_type == ui::mojom::MenuSourceType::kTouch || |
| params.source_type == ui::mojom::MenuSourceType::kLongPress; |
| |
| Java_SelectionPopupControllerImpl_showSelectionMenu( |
| env, obj, params.x, params.y, params.selection_rect.x(), |
| params.selection_rect.y(), params.selection_rect.right(), |
| params.selection_rect.bottom(), handle_height, params.is_editable, |
| is_password_type, jselected_text, params.selection_start_offset, |
| can_select_all, can_edit_richly, should_suggest, |
| static_cast<int>(params.source_type), |
| render_frame_host->GetJavaRenderFrameHost()); |
| return true; |
| } |
| |
| void SelectionPopupController::OnSelectAroundCaretAck( |
| int startOffset, |
| int endOffset, |
| int surroundingTextLength, |
| blink::mojom::SelectAroundCaretResultPtr result) { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) { |
| return; |
| } |
| if (result.is_null() || !IsOffsetAdjustValid(startOffset, endOffset, |
| surroundingTextLength, result)) { |
| Java_SelectionPopupControllerImpl_onSelectAroundCaretFailure(env, obj); |
| return; |
| } |
| |
| Java_SelectionPopupControllerImpl_onSelectAroundCaretSuccess( |
| env, obj, result->extended_start_adjust, result->extended_end_adjust, |
| result->word_start_adjust, result->word_end_adjust); |
| } |
| |
| void SelectionPopupController::HidePopupsAndPreserveSelection() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return; |
| |
| Java_SelectionPopupControllerImpl_hidePopupsAndPreserveSelection(env, obj); |
| } |
| |
| void SelectionPopupController::RestoreSelectionPopupsIfNecessary() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) |
| return; |
| |
| Java_SelectionPopupControllerImpl_restoreSelectionPopupsIfNecessary(env, obj); |
| } |
| |
| void SelectionPopupController::ChildLocalSurfaceIdChanged() { |
| JNIEnv* env = AttachCurrentThread(); |
| ScopedJavaLocalRef<jobject> obj = java_obj_.get(env); |
| if (obj.is_null()) { |
| return; |
| } |
| |
| Java_SelectionPopupControllerImpl_childLocalSurfaceIdChanged(env, obj); |
| } |
| |
| } // namespace content |