diff options
author | boliu@chromium.org <boliu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-08 18:34:05 +0000 |
---|---|---|
committer | boliu@chromium.org <boliu@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-11-08 18:34:05 +0000 |
commit | a7198a1f23bd0fa70e1486e4deb71c1812572e26 (patch) | |
tree | ef9457229bb64e328d7602559c479101b0a11f6b | |
parent | 72bd37ea59cc9b056dda7005e761aaba958e9eca (diff) | |
download | chromium_src-a7198a1f23bd0fa70e1486e4deb71c1812572e26.zip chromium_src-a7198a1f23bd0fa70e1486e4deb71c1812572e26.tar.gz chromium_src-a7198a1f23bd0fa70e1486e4deb71c1812572e26.tar.bz2 |
Implement hit test related methods in Android WebView
The public Android WebView APIs are WebView.getHitTestResult,
WebView.requestFocusNodeHref, and WebView.requestImageRef.
These APIs are mostly used for creating context menus but have
become broken APIs as the Android evolved from the single
threaded initial version of WebView.
* The names are misleading since the value retrieved is based both on
a combination of touch events and FocusedNodeChanged callback
from WebKit.
* The methods are synchronous and there are inherently racy. This is
the case with with the latest version of Android WebView as well.
However this may become more of a problem as Chromium is
multi-processed.
* The methods are inefficient since work needs to be done even if the
client application never call these methods.
This is the first part of largely replicating the broken code with tests
and extensive comments. The goal is to replicate existing logic but
there are probably corner cases that are not matched.
Overall flow is on a touch event or focused node changed event, perform
a WebKit hit test, get the relevant result data and save it in the browser.
The return what is saved when apps query for the data.
Work still needs to be done after this:
* Implement updating hit test data after tab-ing to a link
* Hook up content detection code in content/
* Implement focus highlighting with DPAD controls.
BUG=
All bots are green except unrelated instrumentation tests.
AndroidWebView instrumentation tests are green.
NOTRY=true
Review URL: https://chromiumcodereview.appspot.com/11360037
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@166712 0039d316-1c4b-4281-b951-d872f2087c98
14 files changed, 603 insertions, 3 deletions
diff --git a/android_webview/android_webview.gyp b/android_webview/android_webview.gyp index 3405df6..e0e66f0 100644 --- a/android_webview/android_webview.gyp +++ b/android_webview/android_webview.gyp @@ -80,6 +80,8 @@ 'common/android_webview_message_generator.h', 'common/aw_content_client.cc', 'common/aw_content_client.h', + 'common/aw_hit_test_data.cc', + 'common/aw_hit_test_data.h', 'common/aw_resource.h', 'common/render_view_messages.cc', 'common/render_view_messages.h', diff --git a/android_webview/browser/renderer_host/aw_render_view_host_ext.cc b/android_webview/browser/renderer_host/aw_render_view_host_ext.cc index 66e7134..e94970e 100644 --- a/android_webview/browser/renderer_host/aw_render_view_host_ext.cc +++ b/android_webview/browser/renderer_host/aw_render_view_host_ext.cc @@ -33,9 +33,22 @@ void AwRenderViewHostExt::DocumentHasImages(DocumentHasImagesResult result) { } void AwRenderViewHostExt::ClearCache() { + DCHECK(CalledOnValidThread()); Send(new AwViewMsg_ClearCache); } +void AwRenderViewHostExt::RequestNewHitTestDataAt(int view_x, int view_y) { + DCHECK(CalledOnValidThread()); + Send(new AwViewMsg_DoHitTest(web_contents()->GetRoutingID(), + view_x, + view_y)); +} + +const AwHitTestData& AwRenderViewHostExt::GetLastHitTestData() const { + DCHECK(CalledOnValidThread()); + return last_hit_test_data_; +} + void AwRenderViewHostExt::RenderViewGone(base::TerminationStatus status) { DCHECK(CalledOnValidThread()); for (std::map<int, DocumentHasImagesResult>::iterator pending_req = @@ -51,6 +64,8 @@ bool AwRenderViewHostExt::OnMessageReceived(const IPC::Message& message) { IPC_BEGIN_MESSAGE_MAP(AwRenderViewHostExt, message) IPC_MESSAGE_HANDLER(AwViewHostMsg_DocumentHasImagesResponse, OnDocumentHasImagesResponse) + IPC_MESSAGE_HANDLER(AwViewHostMsg_UpdateHitTestData, + OnUpdateHitTestData) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() @@ -70,4 +85,10 @@ void AwRenderViewHostExt::OnDocumentHasImagesResponse(int msg_id, } } +void AwRenderViewHostExt::OnUpdateHitTestData( + const AwHitTestData& hit_test_data) { + DCHECK(CalledOnValidThread()); + last_hit_test_data_ = hit_test_data; +} + } // namespace android_webview diff --git a/android_webview/browser/renderer_host/aw_render_view_host_ext.h b/android_webview/browser/renderer_host/aw_render_view_host_ext.h index fcde789..3052479 100644 --- a/android_webview/browser/renderer_host/aw_render_view_host_ext.h +++ b/android_webview/browser/renderer_host/aw_render_view_host_ext.h @@ -7,6 +7,7 @@ #include "content/public/browser/web_contents_observer.h" +#include "android_webview/common/aw_hit_test_data.h" #include "base/threading/non_thread_safe.h" namespace android_webview { @@ -28,15 +29,30 @@ class AwRenderViewHostExt : public content::WebContentsObserver, // Clear all WebCore memory cache (not only for this view). void ClearCache(); + // Do a hit test at the view port coordinates and asynchronously update + // |last_hit_test_data_|. + void RequestNewHitTestDataAt(int view_x, int view_y); + + // Return |last_hit_test_data_|. Note that this is unavoidably racy; + // the corresponding public WebView API is as well. + const AwHitTestData& GetLastHitTestData() const; + private: // content::WebContentsObserver implementation. virtual void RenderViewGone(base::TerminationStatus status) OVERRIDE; virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; void OnDocumentHasImagesResponse(int msg_id, bool has_images); + void OnUpdateHitTestData( + const AwHitTestData& hit_test_data); std::map<int, DocumentHasImagesResult> pending_document_has_images_requests_; + // Master copy of hit test data on the browser side. This is updated + // as a result of DoHitTest called explicitly or when the FocusedNodeChanged + // is called in AwRenderViewExt. + AwHitTestData last_hit_test_data_; + DISALLOW_COPY_AND_ASSIGN(AwRenderViewHostExt); }; diff --git a/android_webview/common/aw_hit_test_data.cc b/android_webview/common/aw_hit_test_data.cc new file mode 100644 index 0000000..997b606 --- /dev/null +++ b/android_webview/common/aw_hit_test_data.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "android_webview/common/aw_hit_test_data.h" + +namespace android_webview { + +AwHitTestData::AwHitTestData() : type(UNKNOWN_TYPE) {} + +AwHitTestData::~AwHitTestData() {} + +} // namespace android_webview diff --git a/android_webview/common/aw_hit_test_data.h b/android_webview/common/aw_hit_test_data.h new file mode 100644 index 0000000..a05ea2e --- /dev/null +++ b/android_webview/common/aw_hit_test_data.h @@ -0,0 +1,76 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef ANDROID_WEBVIEW_COMMON_AW_HIT_TEST_DATA_H_ +#define ANDROID_WEBVIEW_COMMON_AW_HIT_TEST_DATA_H_ + +#include "base/string16.h" +#include "googleurl/src/gurl.h" + +namespace android_webview { + +// Holdes all hit test data needed by public WebView APIs. +// The Java counter part to this is AwContents.HitTestData. +struct AwHitTestData { + + // Matches exactly with constants in WebView.HitTestResult, with deprecated + // values removed. + enum Type { + // Default type where nothing we are interested in is hit. + // |extra_data_for_type| will be empty. All other values should be emtpy + // except the special case described below. + // For special case of invalid or javascript scheme url that would + // otherwise be type an LINK type, |href|, |anchor_text|, |img_src| contain + // their normal values for the respective type. + UNKNOWN_TYPE = 0, + + // Content detection types. Not used yet. + // TODO(boliu): Hook up content detection. + PHONE_TYPE = 2, + GEO_TYPE = 3, + EMAIL_TYPE = 4, + + // Hit on a pure image (without links). |extra_data_for_type|, |href|, + // and |anchor_text| will be empty. |img_src| will contain the absolute + // source url of the image. + IMAGE_TYPE = 5, + + // Hit on a link with valid and non-javascript url and without embedded + // image. |extra_data_for_type| is the valid absolute url of the link. + // |href| will contain the exact href attribute string. |anchor_text| will + // contain the anchor text if the link is an anchor tag. |img_src| will be + // empty. + // Note 1: If the link url is invalid or javascript scheme, then the type + // will be UNKNOWN_TYPE. + // Note 2: Note that this matches SRC_ANCHOR_TYPE in the public WebView + // Java API, but the actual tag can be something other than <a>, such as + // <link> or <area>. + SRC_LINK_TYPE = 7, + + // Same as SRC_LINK_TYPE except the link contains an image. |img_src| and + // |extra_data_for_type| will contain the absolute valid url of the image + // source. |href| will contain the (possibly invalid or javascript-scheme) + // link href attribute. |anchor_text| will be empty. + // Both notes from SRC_LINK_TYPE apply. + SRC_IMAGE_LINK_TYPE = 8, + + // Hit on an editable text input element. All other values will be empty. + EDIT_TEXT_TYPE = 9, + }; + + // For all strings/GURLs, empty/invalid will become null upon conversion to + // Java. + int type; // Only values from enum Type above. + std::string extra_data_for_type; + string16 href; + string16 anchor_text; + GURL img_src; + + AwHitTestData(); + ~AwHitTestData(); +}; + +} // namespace android_webview + +#endif // ANDROID_WEBVIEW_COMMON_AW_HIT_TEST_DATA_H_ diff --git a/android_webview/common/render_view_messages.h b/android_webview/common/render_view_messages.h index 96978b1..76f8779 100644 --- a/android_webview/common/render_view_messages.h +++ b/android_webview/common/render_view_messages.h @@ -3,6 +3,8 @@ // found in the LICENSE file. // Multiply-included file, no traditional include guard. +#include "android_webview/common/aw_hit_test_data.h" +#include "content/public/common/common_param_traits.h" #include "ipc/ipc_channel_handle.h" #include "ipc/ipc_message_macros.h" #include "ipc/ipc_platform_file.h" @@ -11,7 +13,6 @@ #ifndef ANDROID_WEBVIEW_COMMON_RENDER_VIEW_MESSAGES_H_ #define ANDROID_WEBVIEW_COMMON_RENDER_VIEW_MESSAGES_H_ - namespace IPC { // TODO - add enums and custom IPC traits here when needed. @@ -20,6 +21,14 @@ namespace IPC { #endif // ANDROID_WEBVIEW_COMMON_RENDER_VIEW_MESSAGES_H_ +IPC_STRUCT_TRAITS_BEGIN(android_webview::AwHitTestData) + IPC_STRUCT_TRAITS_MEMBER(type) + IPC_STRUCT_TRAITS_MEMBER(extra_data_for_type) + IPC_STRUCT_TRAITS_MEMBER(href) + IPC_STRUCT_TRAITS_MEMBER(anchor_text) + IPC_STRUCT_TRAITS_MEMBER(img_src) +IPC_STRUCT_TRAITS_END() + #define IPC_MESSAGE_START AndroidWebViewMsgStart //----------------------------------------------------------------------------- @@ -27,7 +36,7 @@ namespace IPC { // These are messages sent from the browser to the renderer process. // Tells the renderer to drop all WebCore memory cache. -IPC_MESSAGE_CONTROL0(AwViewMsg_ClearCache); +IPC_MESSAGE_CONTROL0(AwViewMsg_ClearCache) // Request for the renderer to determine if the document contains any image // elements. The id should be passed in the response message so the response @@ -35,6 +44,13 @@ IPC_MESSAGE_CONTROL0(AwViewMsg_ClearCache); IPC_MESSAGE_ROUTED1(AwViewMsg_DocumentHasImages, int /* id */) +// Do hit test at the given webview coordinate. "Webview" coordinates are +// physical pixel values with the 0,0 at the top left of the current displayed +// view (ie 0,0 is not the top left of the page if the page is scrolled). +IPC_MESSAGE_ROUTED2(AwViewMsg_DoHitTest, + int /* view_x */, + int /* view_y */) + //----------------------------------------------------------------------------- // RenderView messages // These are messages sent from the renderer to the browser process. @@ -44,3 +60,6 @@ IPC_MESSAGE_ROUTED2(AwViewHostMsg_DocumentHasImagesResponse, int, /* id */ bool /* has_images */) +// Response to AwViewMsg_DoHitTest. +IPC_MESSAGE_ROUTED1(AwViewHostMsg_UpdateHitTestData, + android_webview::AwHitTestData) diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java index e8dd5e9..800f160 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwContents.java +++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java @@ -8,10 +8,12 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.net.http.SslCertificate; import android.os.AsyncTask; +import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.Log; +import android.view.MotionEvent; import android.view.ViewGroup; import android.webkit.ValueCallback; @@ -48,6 +50,35 @@ public class AwContents { private static final String WEB_ARCHIVE_EXTENSION = ".mht"; + /** + * WebKit hit test related data strcutre. These are used to implement + * getHitTestResult, requestFocusNodeHref, requestImageRef methods in WebView. + * All values should be updated together. The native counterpart is + * AwHitTestData. + */ + public static class HitTestData { + // Used in getHitTestResult. + public final int hitTestResultType; + public final String hitTestResultExtraData; + + // Used in requestFocusNodeHref (all three) and requestImageRef (only imgSrc). + public final String href; + public final String anchorText; + public final String imgSrc; + + private HitTestData(int type, + String extra, + String href, + String anchorText, + String imgSrc) { + this.hitTestResultType = type; + this.hitTestResultExtraData = extra; + this.href = href; + this.anchorText = anchorText; + this.imgSrc = imgSrc; + } + } + private int mNativeAwContents; private ContentViewCore mContentViewCore; private AwContentsClient mContentsClient; @@ -366,6 +397,60 @@ public class AwContents { return null; } + /** + * This should be called from the onTouchEvent of the view. + */ + public void considerMotionEventForHitTest(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + int actionIndex = event.getActionIndex(); + + // Note this will trigger IPC back to browser even if nothing is hit. + nativeRequestNewHitTestDataAt(mNativeAwContents, + Math.round(event.getX(actionIndex)), + Math.round(event.getY(actionIndex))); + } + } + + /** + * Method to return all hit test values relevant to public WebView API. + * Note that this expose more data than needed for WebView.getHitTestResult. + */ + public HitTestData getLastHitTestResult() { + return nativeGetLastHitTestData(mNativeAwContents); + } + + /** + * @see android.webkit.WebView#requestFocusNodeHref() + */ + public void requestFocusNodeHref(Message msg) { + if (msg == null) { + return; + } + + HitTestData hitTestData = nativeGetLastHitTestData(mNativeAwContents); + Bundle data = msg.getData(); + data.putString("url", hitTestData.href); + data.putString("title", hitTestData.anchorText); + data.putString("src", hitTestData.imgSrc); + msg.setData(data); + msg.sendToTarget(); + } + + /** + * @see android.webkit.WebView#requestImageRef() + */ + public void requestImageRef(Message msg) { + if (msg == null) { + return; + } + + HitTestData hitTestData = nativeGetLastHitTestData(mNativeAwContents); + Bundle data = msg.getData(); + data.putString("url", hitTestData.imgSrc); + msg.setData(data); + msg.sendToTarget(); + } + //-------------------------------------------------------------------------------------------- // Methods called from native via JNI //-------------------------------------------------------------------------------------------- @@ -395,6 +480,12 @@ public class AwContents { mContentsClient.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); } + @CalledByNative + private static HitTestData createHitTestData( + int type, String extra, String href, String anchorText, String imgSrc) { + return new HitTestData(type, extra, href, anchorText, imgSrc); + } + // ------------------------------------------------------------------------------------------- // Helper methods // ------------------------------------------------------------------------------------------- @@ -493,4 +584,6 @@ public class AwContents { private native void nativeClearMatches(int nativeAwContents); private native void nativeClearCache(int nativeAwContents, boolean includeDiskFiles); private native byte[] nativeGetCertificate(int nativeAwContents); + private native void nativeRequestNewHitTestDataAt(int nativeAwContents, int x, int y); + private native HitTestData nativeGetLastHitTestData(int nativeAwContents); } diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/WebKitHitTestTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/WebKitHitTestTest.java new file mode 100644 index 0000000..1abf4f7 --- /dev/null +++ b/android_webview/javatests/src/org/chromium/android_webview/test/WebKitHitTestTest.java @@ -0,0 +1,247 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview.test; + +import android.os.SystemClock; +import android.test.FlakyTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.MotionEvent; +import android.webkit.WebView.HitTestResult; + +import org.chromium.android_webview.AwContents; +import org.chromium.base.test.util.Feature; +import org.chromium.android_webview.test.util.CommonResources; +import org.chromium.content.browser.test.util.Criteria; +import org.chromium.content.browser.test.util.CriteriaHelper; +import org.chromium.net.test.util.TestWebServer; + +import java.util.concurrent.Callable; + +public class WebKitHitTestTest extends AndroidWebViewTestBase { + private TestAwContentsClient mContentsClient; + private AwTestContainerView mTestView; + private AwContents mAwContents; + private TestWebServer mWebServer; + + private static String HREF = "http://foo/"; + private static String ANCHOR_TEXT = "anchor text"; + + @Override + public void setUp() throws Exception { + super.setUp(); + mContentsClient = new TestAwContentsClient(); + mTestView = createAwTestContainerViewOnMainSync(mContentsClient); + mAwContents = mTestView.getAwContents(); + mWebServer = new TestWebServer(false); + } + + @Override + public void tearDown() throws Exception { + // TODO(boliu): This is to work around disk cache corruption bug on + // unclean shutdown (crbug.com/154805). + try { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mAwContents.clearCache(true); + } + }); + } catch (Throwable e) { + throw new Exception(e); + } + if (mWebServer != null) { + mWebServer.shutdown(); + } + super.tearDown(); + } + + private void setServerResponseAndLoad(String response) throws Throwable { + String url = mWebServer.setResponse("/hittest.html", response, null); + loadUrlSync(mAwContents, + mContentsClient.getOnPageFinishedHelper(), + url); + } + + private static String fullPageLink(String href, String anchorText) { + return CommonResources.makeHtmlPageFrom("", "<a class=\"full_view\" href=\"" + + href + "\" " + "onclick=\"return false;\">" + anchorText + "</a>"); + } + + private void simulateTouchCenterOfWebViewOnUiThread() throws Throwable { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + long eventTime = SystemClock.uptimeMillis(); + mAwContents.considerMotionEventForHitTest(MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_DOWN, + (float)(mTestView.getRight() - mTestView.getLeft()) / 2, + (float)(mTestView.getBottom() - mTestView.getTop()) / 2, + 0)); + } + }); + } + + private boolean pollOnUiThread(final Callable<Boolean> callable) throws Throwable { + return CriteriaHelper.pollForCriteria(new Criteria() { + @Override + public boolean isSatisfied() { + try { + return runTestOnUiThreadAndGetResult(callable); + } catch (Throwable e) { + return false; + } + } + }); + } + + private boolean pollForHitTestDataOnUiThread( + final int type, final String extra) throws Throwable { + return pollOnUiThread(new Callable<Boolean>() { + @Override + public Boolean call() { + AwContents.HitTestData data = mAwContents.getLastHitTestResult(); + return type == data.hitTestResultType && + (extra == null ? data.hitTestResultExtraData == null : + extra.equals(data.hitTestResultExtraData)); + } + }); + } + + private boolean pollForHrefAndImageSrcOnUiThread( + final String href, + final String anchorText, + final String imageSrc) throws Throwable { + return pollOnUiThread(new Callable<Boolean>() { + @Override + public Boolean call() { + AwContents.HitTestData data = mAwContents.getLastHitTestResult(); + return (href == null ? data.href == null : + href.equals(data.href)) && + (anchorText == null ? data.anchorText == null : + anchorText.equals(data.anchorText)) && + (imageSrc == null ? data.imgSrc == null : + imageSrc.equals(data.imgSrc)); + } + }); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testSrcAnchorType() throws Throwable { + String page = fullPageLink(HREF, ANCHOR_TEXT); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread(HitTestResult.SRC_ANCHOR_TYPE, HREF)); + assertTrue(pollForHrefAndImageSrcOnUiThread(HREF, ANCHOR_TEXT, null)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testSrcAnchorTypeRelativeUrl() throws Throwable { + String relpath = "/foo.html"; + String fullpath = mWebServer.getResponseUrl(relpath); + String page = fullPageLink(relpath, ANCHOR_TEXT); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread( + HitTestResult.SRC_ANCHOR_TYPE, fullpath)); + assertTrue(pollForHrefAndImageSrcOnUiThread(relpath, ANCHOR_TEXT, null)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testSrcImgeAnchorType() throws Throwable { + String relImageSrc = "/nonexistent.jpg"; + String fullImageSrc = mWebServer.getResponseUrl(relImageSrc); + String page = CommonResources.makeHtmlPageFrom("", "<a class=\"full_view\" href=\"" + + HREF + "\"onclick=\"return false;\"><img class=\"full_view\" src=\"" + + relImageSrc + "\"></a>"); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread( + HitTestResult.SRC_IMAGE_ANCHOR_TYPE, fullImageSrc)); + assertTrue(pollForHrefAndImageSrcOnUiThread(HREF, null, fullImageSrc)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testImgeType() throws Throwable { + String relImageSrc = "/nonexistent2.jpg"; + String fullImageSrc = mWebServer.getResponseUrl(relImageSrc); + String page = CommonResources.makeHtmlPageFrom("", + "<img class=\"full_view\" src=\"" + relImageSrc + "\">"); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread( + HitTestResult.IMAGE_TYPE, fullImageSrc)); + assertTrue(pollForHrefAndImageSrcOnUiThread(null, null, fullImageSrc)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testEditTextType() throws Throwable { + String page = CommonResources.makeHtmlPageFrom("", + "<form><input class=\"full_view\" type=\"text\" name=\"test\"></form>"); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread( + HitTestResult.EDIT_TEXT_TYPE, null)); + assertTrue(pollForHrefAndImageSrcOnUiThread(null, null, null)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testUnknownTypeJavascriptScheme() throws Throwable { + // Per documentation, javascript urls are special. + String javascript = "javascript:alert('foo');"; + String page = fullPageLink(javascript, ANCHOR_TEXT); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHrefAndImageSrcOnUiThread(javascript, ANCHOR_TEXT, null)); + assertTrue(pollForHitTestDataOnUiThread(HitTestResult.UNKNOWN_TYPE, null)); + } + + /* + * @SmallTest + * @Feature({"AndroidWebView", "WebKitHitTest"}) + * BUG=158284 + */ + @FlakyTest + public void testUnknownTypeUnrecognizedNode() throws Throwable { + // Since UNKNOWN_TYPE is the default, hit test another type first for + // this test to be valid. + testSrcAnchorType(); + + String page = CommonResources.makeHtmlPageFrom("", + "<div class=\"full_view\">div text</div>"); + setServerResponseAndLoad(page); + simulateTouchCenterOfWebViewOnUiThread(); + assertTrue(pollForHitTestDataOnUiThread(HitTestResult.UNKNOWN_TYPE, null)); + } +} diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/util/CommonResources.java b/android_webview/javatests/src/org/chromium/android_webview/test/util/CommonResources.java index f7ccb0f..421f5da 100644 --- a/android_webview/javatests/src/org/chromium/android_webview/test/util/CommonResources.java +++ b/android_webview/javatests/src/org/chromium/android_webview/test/util/CommonResources.java @@ -94,6 +94,7 @@ public class CommonResources { // any fancy hit target calculations when synthesizing the touch event // to click it. "img.big { width:100%; height:100%; background-color:blue; }" + + ".full_view { height:100%; width:100%; position:absolute; }" + "</style>" + headers + "</head>" + diff --git a/android_webview/native/aw_contents.cc b/android_webview/native/aw_contents.cc index e5308a8..9da03b3 100644 --- a/android_webview/native/aw_contents.cc +++ b/android_webview/native/aw_contents.cc @@ -6,6 +6,7 @@ #include "android_webview/browser/net_disk_cache_remover.h" #include "android_webview/browser/renderer_host/aw_render_view_host_ext.h" +#include "android_webview/common/aw_hit_test_data.h" #include "android_webview/native/aw_browser_dependency_factory.h" #include "android_webview/native/aw_contents_io_thread_client_impl.h" #include "android_webview/native/aw_web_contents_delegate.h" @@ -329,4 +330,39 @@ base::android::ScopedJavaLocalRef<jbyteArray> reinterpret_cast<const uint8*>(der_string.data()), der_string.length()); } +void AwContents::RequestNewHitTestDataAt(JNIEnv* env, jobject obj, + jint x, jint y) { + render_view_host_ext_->RequestNewHitTestDataAt(x, y); +} + +base::android::ScopedJavaLocalRef<jobject> AwContents::GetLastHitTestData( + JNIEnv* env, jobject obj) { + const AwHitTestData& data = render_view_host_ext_->GetLastHitTestData(); + + // Make sure to null the Java object if data is empty/invalid. + ScopedJavaLocalRef<jstring> extra_data_for_type; + if (data.extra_data_for_type.length()) + extra_data_for_type = ConvertUTF8ToJavaString( + env, data.extra_data_for_type); + + ScopedJavaLocalRef<jstring> href; + if (data.href.length()) + href = ConvertUTF16ToJavaString(env, data.href); + + ScopedJavaLocalRef<jstring> anchor_text; + if (data.anchor_text.length()) + anchor_text = ConvertUTF16ToJavaString(env, data.anchor_text); + + ScopedJavaLocalRef<jstring> img_src; + if (data.img_src.is_valid()) + img_src = ConvertUTF8ToJavaString(env, data.img_src.spec()); + + return Java_AwContents_createHitTestData(env, + data.type, + extra_data_for_type.obj(), + href.obj(), + anchor_text.obj(), + img_src.obj()); +} + } // namespace android_webview diff --git a/android_webview/native/aw_contents.h b/android_webview/native/aw_contents.h index 04e24c2..8728192 100644 --- a/android_webview/native/aw_contents.h +++ b/android_webview/native/aw_contents.h @@ -72,6 +72,9 @@ class AwContents : public FindHelper::Listener { jobject delegate); base::android::ScopedJavaLocalRef<jbyteArray> GetCertificate( JNIEnv* env, jobject obj); + void RequestNewHitTestDataAt(JNIEnv* env, jobject obj, jint x, jint y); + base::android::ScopedJavaLocalRef<jobject> GetLastHitTestData( + JNIEnv* env, jobject obj); // Find-in-page API and related methods. jint FindAllSync(JNIEnv* env, jobject obj, jstring search_string); diff --git a/android_webview/renderer/DEPS b/android_webview/renderer/DEPS index 5ee0454..5b0050c 100644 --- a/android_webview/renderer/DEPS +++ b/android_webview/renderer/DEPS @@ -5,5 +5,6 @@ include_rules = [ "+content/public/renderer", - "+third_party/WebKit/Source/WebKit/chromium", + "+third_party/WebKit/Source/WebKit/chromium/public", + "+third_party/WebKit/Source/Platform/chromium/public", ] diff --git a/android_webview/renderer/aw_render_view_ext.cc b/android_webview/renderer/aw_render_view_ext.cc index 2196ffe..3369379 100644 --- a/android_webview/renderer/aw_render_view_ext.cc +++ b/android_webview/renderer/aw_render_view_ext.cc @@ -4,16 +4,21 @@ #include "android_webview/renderer/aw_render_view_ext.h" +#include "android_webview/common/aw_hit_test_data.h" #include "android_webview/common/render_view_messages.h" #include "content/public/common/url_constants.h" +#include "content/public/common/url_constants.h" #include "content/public/renderer/document_state.h" #include "content/public/renderer/render_view.h" +#include "third_party/WebKit/Source/Platform/chromium/public/WebSize.h" #include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebURL.h" #include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebVector.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebDataSource.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebDocument.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebElement.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebHitTestResult.h" +#include "third_party/WebKit/Source/WebKit/chromium/public/WebNode.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebSecurityOrigin.h" #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h" @@ -35,6 +40,7 @@ bool AwRenderViewExt::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(AwRenderViewExt, message) IPC_MESSAGE_HANDLER(AwViewMsg_DocumentHasImages, OnDocumentHasImagesRequest) + IPC_MESSAGE_HANDLER(AwViewMsg_DoHitTest, OnDoHitTest) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; @@ -79,4 +85,66 @@ void AwRenderViewExt::DidCommitProvisionalLoad(WebKit::WebFrame* frame, } } +void AwRenderViewExt::FocusedNodeChanged(const WebKit::WebNode& node) { + if (!node.isNull()) { + if (node.isTextNode() && node.isContentEditable()) { + AwHitTestData data; + data.type = AwHitTestData::EDIT_TEXT_TYPE; + Send(new AwViewHostMsg_UpdateHitTestData( + routing_id(), data)); + } else { + // TODO(boliu): Implement this path. + NOTIMPLEMENTED() << "Tab focused links not implemented"; + } + } +} + +void AwRenderViewExt::OnDoHitTest(int view_x, int view_y) { + if (!render_view() || !render_view()->GetWebView()) + return; + + const WebKit::WebHitTestResult result = + render_view()->GetWebView()->hitTestResultAt( + WebKit::WebPoint(view_x, view_y)); + AwHitTestData data; + + // Populate fixed AwHitTestData fields. + if (result.absoluteImageURL().isValid()) + data.img_src = result.absoluteImageURL(); + if (!result.urlElement().isNull()) { + data.anchor_text = result.urlElement().innerText(); + + // href is the actual 'href' attribute, which might relative if valid or can + // possibly contain garbage otherwise, so not using absoluteLinkURL here. + data.href = result.urlElement().getAttribute("href"); + } + + GURL url(result.absoluteLinkURL()); + bool is_javascript_scheme = url.SchemeIs(chrome::kJavaScriptScheme); + + // Set AwHitTestData type and extra_data_for_type. + if (result.absoluteLinkURL().isValid() && + !result.absoluteImageURL().isValid() && + !is_javascript_scheme) { + data.type = AwHitTestData::SRC_LINK_TYPE; + data.extra_data_for_type = url.spec(); + } else if (result.absoluteLinkURL().isValid() && + result.absoluteImageURL().isValid() && + !is_javascript_scheme) { + data.type = AwHitTestData::SRC_IMAGE_LINK_TYPE; + data.extra_data_for_type = data.img_src.spec(); + } else if (!result.absoluteLinkURL().isValid() && + result.absoluteImageURL().isValid()) { + data.type = AwHitTestData::IMAGE_TYPE; + data.extra_data_for_type = data.img_src.spec(); + } else if (result.isContentEditable()) { + data.type = AwHitTestData::EDIT_TEXT_TYPE; + DCHECK(data.extra_data_for_type.length() == 0); + } else { + // TODO(boliu): Do content detection here. + } + + Send(new AwViewHostMsg_UpdateHitTestData(routing_id(), data)); +} + } // namespace android_webview diff --git a/android_webview/renderer/aw_render_view_ext.h b/android_webview/renderer/aw_render_view_ext.h index d2ff20c..c6d3bba 100644 --- a/android_webview/renderer/aw_render_view_ext.h +++ b/android_webview/renderer/aw_render_view_ext.h @@ -12,6 +12,7 @@ namespace WebKit { +class WebNode; class WebURL; } // namespace WebKit @@ -34,9 +35,12 @@ class AwRenderViewExt : public content::RenderViewObserver, virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; virtual void DidCommitProvisionalLoad(WebKit::WebFrame* frame, bool is_new_navigation) OVERRIDE; + virtual void FocusedNodeChanged(const WebKit::WebNode& node) OVERRIDE; void OnDocumentHasImagesRequest(int id); + void OnDoHitTest(int view_x, int view_y); + // WebKit::WebPermissionClient implementation. virtual bool allowImage(WebKit::WebFrame* frame, bool enabledPerSettings, |