chromium/content/browser/accessibility/web_contents_accessibility_android.h

// 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.

#ifndef CONTENT_BROWSER_ACCESSIBILITY_WEB_CONTENTS_ACCESSIBILITY_ANDROID_H_
#define CONTENT_BROWSER_ACCESSIBILITY_WEB_CONTENTS_ACCESSIBILITY_ANDROID_H_

#include <unordered_map>

#include "base/android/jni_string.h"
#include "base/android/jni_weak_ref.h"
#include "base/android/scoped_java_ref.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/web_contents_accessibility.h"
#include "content/common/content_export.h"
#include "ui/accessibility/platform/ax_node_id_delegate.h"
#include "ui/gfx/geometry/size.h"

namespace ui {
class MotionEventAndroid;
struct AXTreeUpdate;
}

namespace content {

namespace {
// The maximum number of TYPE_WINDOW_CONTENT_CHANGED events to fire in one
// atomic update before we give up and fire it on the root node instead.
constexpr int kMaxContentChangedEventsToFire = 5;

// The number of 'ticks' on a slider when no step value is defined. The value
// of 20 implies 20 steps, or a 5% move with each increment/decrement action.
constexpr int kDefaultNumberOfTicksForSliders = 20;

// Max dimensions for the image data of a node.
constexpr gfx::Size kMaxImageSize = gfx::Size(2000, 2000);
}  // namespace

class BrowserAccessibilityAndroid;
class BrowserAccessibilityManagerAndroid;
class WebContents;
class WebContentsImpl;

// Bridges BrowserAccessibilityManagerAndroid and Java WebContentsAccessibility.
// A RenderWidgetHostConnector runs behind to manage the connection. Referenced
// by BrowserAccessibilityManagerAndroid for main frame only.
// The others for subframes should acquire this instance through the root
// manager to access Java layer.
//
// Owned by |Connector|, and destroyed together when the associated web contents
// is destroyed.
class CONTENT_EXPORT WebContentsAccessibilityAndroid
    : public WebContentsAccessibility,
      public ui::AXNodeIdDelegate {
 public:
  WebContentsAccessibilityAndroid(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& obj,
      WebContents* web_contents,
      const base::android::JavaParamRef<jobject>&
          jaccessibility_node_info_builder);
  WebContentsAccessibilityAndroid(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& obj,
      jlong ax_tree_update_ptr,
      const base::android::JavaParamRef<jobject>&
          jaccessibility_node_info_builder);
  WebContentsAccessibilityAndroid(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& obj,
      const base::android::JavaParamRef<jobject>& jassist_data_builder,
      WebContents* web_contents);

  WebContentsAccessibilityAndroid(const WebContentsAccessibilityAndroid&) =
      delete;
  WebContentsAccessibilityAndroid& operator=(
      const WebContentsAccessibilityAndroid&) = delete;

  ~WebContentsAccessibilityAndroid() override;

  // ui::AXNodeIdDelegate:
  ui::AXPlatformNodeId GetOrCreateAXNodeUniqueId(
      ui::AXNodeID ax_node_id) override;
  void OnAXNodeDeleted(ui::AXNodeID ax_node_id) override;

  // Notify the root BrowserAccessibilityManager that this is the
  // WebContentsAccessibilityAndroid it should talk to.
  void UpdateBrowserAccessibilityManager();

  // --------------------------------------------------------------------------
  // Methods called from Java via JNI
  // --------------------------------------------------------------------------

  void DeleteEarly(JNIEnv* env);

  // To communicate over the JNI bridge, a BrowserAccessibilityManager needs to
  // have a reference to |this| object. There may be multiple BAMs for a given
  // frame, but on the Java-side there will be one WebContentsAccessibilityImpl.
  // We connect only the root BAM to WCAI through a WeakPtr to |this| instance.
  // We get the root BAM from the primary frame of the RenderFrameHostImpl for
  // the webContents that is associated with this instance.
  //
  // Note: The root BAM may be null during construction, unless the BAM creation
  // precedes render view updates for the associated web contents. If the root
  // BAM is still null, this method does not connect the instances. The
  // Java-side code will make a connection request on every attempt the Android
  // Framework makes to get an AccessibilityNodeProvider, until the root manager
  // is connected to |this| (See #IsRootManagerConnected, below). This may
  // happen multiple times. See WebContentsAccessibilityImpl.java for more info.
  void ConnectInstanceToRootManager(JNIEnv* env);
  jboolean IsRootManagerConnected(JNIEnv* env);

  // This method should only be used by the Auto-Disable accessibility feature.
  //
  // This method "turns off" the renderer-side accessibility engine. First, it
  // will reset the weak reference that the root BAM has to |this| (which will
  // disable the C++ -> Java bridge), then it will clear objects in memory.
  //
  // Note: Calling this method should be preceded by calling {SetBrowserAXMode}
  void DisableRendererAccessibility(JNIEnv* env);

  // This method should only be used by the Auto-Disable accessibility feature.
  //
  // This method "turns on" the renderer-side accessibility engine, and builds
  // the connections needed to communicate over the C++ -> Java bridge. It will
  // perform the opposite operation as the teardown method above.
  //
  // Note: Calling this method should be followed by calling {SetBrowserAXMode}
  void ReEnableRendererAccessibility(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& jweb_contents);

  base::android::ScopedJavaLocalRef<jstring> GetSupportedHtmlElementTypes(
      JNIEnv* env);

  void SetAllowImageDescriptions(JNIEnv* env,
                                 jboolean allow_image_descriptions);
  void SetPasswordRules(JNIEnv* env,
                        jboolean should_respect_displayed_password_text,
                        jboolean should_expost_password_text);

  // Tree methods.
  jint GetRootId(JNIEnv* env);
  jboolean IsNodeValid(JNIEnv* env, jint id);

  void HitTest(JNIEnv* env, jint x, jint y);

  // Methods to get information about a specific node.
  jboolean IsEditableText(JNIEnv* env, jint id);
  jboolean IsFocused(JNIEnv* env, jint id);
  jint GetEditableTextSelectionStart(JNIEnv* env, jint id);
  jint GetEditableTextSelectionEnd(JNIEnv* env, jint id);
  base::android::ScopedJavaLocalRef<jintArray> GetAbsolutePositionForNode(
      JNIEnv* env,
      jint unique_id);

  // Populate Java accessibility data structures with info about a node.
  jboolean UpdateCachedAccessibilityNodeInfo(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& info,
      jint id);
  jboolean PopulateAccessibilityNodeInfo(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& info,
      jint id);
  jboolean PopulateAccessibilityEvent(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& event,
      jint id,
      jint event_type);

  // Perform actions.
  void Click(JNIEnv* env, jint id);
  void Focus(JNIEnv* env, jint id);
  void Blur(JNIEnv* env);
  void ScrollToMakeNodeVisible(JNIEnv* env, jint id);
  void SetTextFieldValue(JNIEnv* env,
                         jint id,
                         const base::android::JavaParamRef<jstring>& value);
  void SetSelection(JNIEnv* env, jint id, jint start, jint end);
  jboolean AdjustSlider(JNIEnv* env, jint id, jboolean increment);
  void ShowContextMenu(JNIEnv* env, jint id);

  // Return the id of the next node in tree order in the direction given by
  // |forwards|, starting with |start_id|, that matches |element_type|,
  // where |element_type| is a special uppercase string from TalkBack or
  // BrailleBack indicating general categories of web content like
  // "SECTION" or "CONTROL".  Return 0 if not found.
  // Use |can_wrap_to_last_element| to specify if a backwards search can wrap
  // around to the last element. This is used to expose the last HTML element
  // upon swiping backwards into a WebView.
  jint FindElementType(JNIEnv* env,
                       jint start_id,
                       const base::android::JavaParamRef<jstring>& element_type,
                       jboolean forwards,
                       jboolean can_wrap_to_last_element,
                       jboolean use_default_predicate);

  // Respond to a ACTION_[NEXT/PREVIOUS]_AT_MOVEMENT_GRANULARITY action
  // and move the cursor/selection within the given node id. We keep track
  // of our own selection in BrowserAccessibilityManager.java for static
  // text, but if this is an editable text node, updates the selected text
  // in Blink, too, and either way calls
  // Java_BrowserAccessibilityManager_finishGranularityMove[NEXT/PREVIOUS]
  // with the result.
  jboolean NextAtGranularity(JNIEnv* env,
                             jint granularity,
                             jboolean extend_selection,
                             jint id,
                             jint cursor_index);
  jboolean PreviousAtGranularity(JNIEnv* env,
                                 jint granularity,
                                 jboolean extend_selection,
                                 jint id,
                                 jint cursor_index);

  // Move accessibility focus. This sends a message to the renderer to
  // clear accessibility focus on the previous node and set accessibility
  // focus on the current node. This isn't exposed to the open web, but used
  // internally.
  //
  // In addition, when a node gets accessibility focus we asynchronously
  // load inline text boxes for this node only, enabling more accurate
  // movement by granularities on this node.
  void MoveAccessibilityFocus(JNIEnv* env,
                              jint old_unique_id,
                              jint new_unique_id);

  // Returns true if the object is a slider.
  bool IsSlider(JNIEnv* env, jint id);

  // Accessibility methods to support navigation for autofill popup.
  void OnAutofillPopupDisplayed(JNIEnv* env);
  void OnAutofillPopupDismissed(JNIEnv* env);
  jint GetIdForElementAfterElementHostingAutofillPopup(JNIEnv* env);
  jboolean IsAutofillPopupNode(JNIEnv* env, jint id);

  // Scrolls any scrollable container by about 80% of one page in the
  // given direction, or 100% in the case of page scrolls.
  bool Scroll(JNIEnv* env, jint id, int direction, bool is_page_scroll);

  // Sets value for range type nodes.
  bool SetRangeValue(JNIEnv* env, jint id, float value);

  // Responds to a hover event without relying on the renderer for hit testing.
  bool OnHoverEventNoRenderer(JNIEnv* env, jfloat x, jfloat y);

  // Returns true if the given subtree has inline text box data, or if there
  // aren't any to load.
  jboolean AreInlineTextBoxesLoaded(JNIEnv* env, jint id);

  // Returns the length of the text node.
  jint GetTextLength(JNIEnv* env, jint id);

  // Add a fake spelling error for testing spelling spannables.
  void AddSpellingErrorForTesting(JNIEnv* env,
                                  jint id,
                                  jint start_offset,
                                  jint end_offset);

  // Request loading inline text boxes for a given node.
  void LoadInlineTextBoxes(JNIEnv* env, jint id);

  // Get the bounds of each character for a given static text node,
  // starting from index |start| with length |len|. The resulting array
  // of ints is 4 times the length |len|, with the bounds being returned
  // as (left, top, right, bottom) in that order corresponding to a
  // android.graphics.RectF.
  base::android::ScopedJavaLocalRef<jintArray>
  GetCharacterBoundingBoxes(JNIEnv* env, jint id, jint start, jint len);

  // Get the image data for a given node. If no image data is available, this
  // will call through to |BrowserAccessibilityManager| to populate the data
  // asynchronously so the next time the method is called the data is ready.
  jboolean GetImageData(JNIEnv* env,
                        const base::android::JavaParamRef<jobject>& info,
                        jint unique_id,
                        jboolean has_sent_previous_request);

  void UpdateFrameInfo(float page_scale);

  // Set a new max for TYPE_WINDOW_CONTENT_CHANGED events to fire.
  void SetMaxContentChangedEventsToFireForTesting(JNIEnv* env, jint maxEvents) {
    // Consider a new |maxEvents| value of -1 to mean to reset to the default.
    if (maxEvents == -1) {
      max_content_changed_events_to_fire_ = kMaxContentChangedEventsToFire;
    } else {
      max_content_changed_events_to_fire_ = maxEvents;
    }
  }

  // Get the current max for TYPE_WINDOW_CONTENT_CHANGED events to fire.
  jint GetMaxContentChangedEventsToFireForTesting(JNIEnv* env) {
    return max_content_changed_events_to_fire_;
  }

  // Reset count of content changed events fired this atomic update.
  void ResetContentChangedEventsCounter() { content_changed_events_ = 0; }

  // Call the BrowserAccessibilityManager to trigger an kEndOfTest event.
  void SignalEndOfTestForTesting(JNIEnv* env);

  // Helper methods to wrap strings with a JNI-friendly cache.
  // Note: This cache is only meant for common strings that might be shared
  //       across many nodes (e.g. role or role description), which have a
  //       finite number of possibilities. Do not use it for page content.
  const base::android::ScopedJavaGlobalRef<jstring>& GetCanonicalJNIString(
      JNIEnv* env,
      std::string str) {
    return GetCanonicalJNIString(env, base::UTF8ToUTF16(str));
  }

  const base::android::ScopedJavaGlobalRef<jstring>& GetCanonicalJNIString(
      JNIEnv* env,
      std::u16string str) {
    auto& slot = common_string_cache_[str];
    if (!slot) {
      // Otherwise, convert the string and add it to the cache, then return.
      slot = base::android::ConvertUTF16ToJavaString(env, str);
      DCHECK(common_string_cache_.size() < 500);
    }

    return slot;
  }

  void RequestAccessibilityTreeSnapshot(
      JNIEnv* env,
      const base::android::JavaParamRef<jobject>& view_structure_root,
      const base::android::JavaParamRef<jobject>& accessibility_coordinates,
      const base::android::JavaParamRef<jobject>& view,
      const base::android::JavaParamRef<jobject>& on_done_callback);

  void ProcessCompletedAccessibilityTreeSnapshot(
      JNIEnv* env,
      const base::android::JavaRef<jobject>& view_structure_root,
      ui::AXTreeUpdate& result);

  void RecursivelyPopulateViewStructureTree(
      JNIEnv* env,
      base::android::ScopedJavaLocalRef<jobject> obj,
      const BrowserAccessibilityAndroid* node,
      const base::android::JavaRef<jobject>& java_side_assist_data_object,
      bool is_root);

  void PopulateViewStructureNode(
      JNIEnv* env,
      base::android::ScopedJavaLocalRef<jobject> obj,
      const BrowserAccessibilityAndroid* node,
      const base::android::JavaRef<jobject>& java_side_assist_data_object);

  // --------------------------------------------------------------------------
  // Methods called from the BrowserAccessibilityManager
  // --------------------------------------------------------------------------

  // State values that affect tree/node construction, so they must be called
  // from the BrowserAccessibilityManagerAndroid. The value of these depends on
  // user settings available in Java-side code, passed here through the JNI.
  bool should_allow_image_descriptions() const {
    return allow_image_descriptions_;
  }

  void HandlePageLoaded(int32_t unique_id);
  void HandleContentChanged(int32_t unique_id);
  void HandleFocusChanged(int32_t unique_id);
  void HandleCheckStateChanged(int32_t unique_id);
  void HandleStateDescriptionChanged(int32_t unique_id);
  void HandleClicked(int32_t unique_id);
  void HandleScrollPositionChanged(int32_t unique_id);
  void HandleScrolledToAnchor(int32_t unique_id);
  void HandleDialogModalOpened(int32_t unique_id);
  void AnnounceLiveRegionText(const std::u16string& text);
  void HandleTextContentChanged(int32_t unique_id);
  void HandleTextSelectionChanged(int32_t unique_id);
  void HandleEditableTextChanged(int32_t unique_id);
  void HandleSliderChanged(int32_t unique_id);
  void SendDelayedWindowContentChangedEvent();
  bool OnHoverEvent(const ui::MotionEventAndroid& event);
  void HandleHover(int32_t unique_id);
  void HandleNavigate(int32_t root_id);
  void UpdateMaxNodesInCache();
  void ClearNodeInfoCacheForGivenId(int32_t unique_id);
  void HandleEndOfTestSignal();
  std::u16string GenerateAccessibilityNodeInfoString(int32_t unique_id);

  base::WeakPtr<WebContentsAccessibilityAndroid> GetWeakPtr();

 private:
  BrowserAccessibilityManagerAndroid* GetRootBrowserAccessibilityManager();

  BrowserAccessibilityAndroid* GetAXFromUniqueID(int32_t unique_id);

  void UpdateAccessibilityNodeInfoBoundsRect(
      JNIEnv* env,
      const base::android::ScopedJavaLocalRef<jobject>& obj,
      const base::android::JavaParamRef<jobject>& info,
      jint id,
      BrowserAccessibilityAndroid* node);

  // A weak reference to the Java WebContentsAccessibilityAndroid object.
  JavaObjectWeakGlobalRef java_ref_;
  JavaObjectWeakGlobalRef java_anib_ref_;

  // A weak reference to the AssistData tree builder which will only be
  // instantiated after a request from the Android framework.
  JavaObjectWeakGlobalRef java_adb_ref_;

  raw_ptr<WebContentsImpl> web_contents_;

  // Used by the accessibility tree snapshotter when snapshot is completed.
  base::android::ScopedJavaGlobalRef<jobject> on_done_callback_;
  base::android::ScopedJavaGlobalRef<jobject> accessibility_coordinates_;
  base::android::ScopedJavaGlobalRef<jobject> view_;

  bool frame_info_initialized_;

  // True if this instance should allow image descriptions, false if the
  // feature should be disabled (dependent on embedder behavior). Default false.
  bool allow_image_descriptions_ = false;

  float page_scale_ = 1.f;

  // Current max number of events to fire, mockable for unit tests
  int max_content_changed_events_to_fire_ = kMaxContentChangedEventsToFire;

  // A count of the number of TYPE_WINDOW_CONTENT_CHANGED events we've
  // fired during a single atomic update.
  int content_changed_events_ = 0;

  // An unordered map of |jstring| objects for classname, role, role
  // description, invalid error, and language strings that are a finite set of
  // strings that need to regularly be converted to Java strings and passed
  // over the JNI.
  std::unordered_map<std::u16string,
                     base::android::ScopedJavaGlobalRef<jstring>>
      common_string_cache_;

  // Manages the connection between web contents and the RenderFrameHost that
  // receives accessibility events.
  // Owns itself, and destroyed upon WebContentsObserver::WebContentsDestroyed.
  class Connector;
  raw_ptr<Connector> connector_ = nullptr;

  // This isn't associated with a real WebContents and is only populated when
  // this class is constructed with a ui::AXTreeUpdate.
  std::unique_ptr<BrowserAccessibilityManagerAndroid> snapshot_root_manager_;

  base::WeakPtrFactory<WebContentsAccessibilityAndroid> weak_ptr_factory_{this};
};

}  // namespace content

#endif  // CONTENT_BROWSER_ACCESSIBILITY_WEB_CONTENTS_ACCESSIBILITY_ANDROID_H_