diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentCaptureTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentCaptureTest.java index ebb2742922636..0fad978e45365 100644 --- a/android_webview/javatests/src/org/chromium/android_webview/test/AwContentCaptureTest.java +++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwContentCaptureTest.java @@ -11,6 +11,8 @@ import android.view.View; import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; +import org.json.JSONArray; +import org.json.JSONTokener; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -26,11 +28,13 @@ import org.chromium.components.content_capture.ContentCaptureConsumer; import org.chromium.components.content_capture.ContentCaptureData; import org.chromium.components.content_capture.ContentCaptureDataBase; import org.chromium.components.content_capture.ContentCaptureFrame; +import org.chromium.components.content_capture.ContentCaptureTestSupport; import org.chromium.components.content_capture.FrameSession; import org.chromium.components.content_capture.OnscreenContentProvider; import org.chromium.components.content_capture.UrlAllowlist; import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.net.test.util.TestWebServer; +import org.chromium.url.GURL; import java.util.ArrayList; import java.util.Arrays; @@ -55,6 +59,7 @@ public class AwContentCaptureTest { public static final int CONTENT_REMOVED = 3; public static final int SESSION_REMOVED = 4; public static final int TITLE_UPDATED = 5; + public static final int FAVICON_UPDATED = 6; public TestAwContentCaptureConsumer() { mCapturedContentIds = new HashSet<Long>(); @@ -113,6 +118,13 @@ public class AwContentCaptureTest { mCallbackHelper.notifyCalled(); } + @Override + public void onFaviconUpdated(ContentCaptureFrame contentCaptureFrame) { + mFaviconUpdatedFrame = contentCaptureFrame; + mCallbacks.add(FAVICON_UPDATED); + mCallbackHelper.notifyCalled(); + } + @Override public boolean shouldCapture(String[] urls) { if (mUrlAllowlist == null) return true; @@ -131,6 +143,10 @@ public class AwContentCaptureTest { return mUpdatedContent; } + public ContentCaptureFrame getFaviconUpdatedFrame() { + return mFaviconUpdatedFrame; + } + public FrameSession getCurrentFrameSession() { return mCurrentFrameSession; } @@ -191,6 +207,7 @@ public class AwContentCaptureTest { private volatile FrameSession mRemovedSession; private volatile long[] mRemovedIds; private volatile ContentCaptureFrame mTitleUpdatedFrame; + private volatile ContentCaptureFrame mFaviconUpdatedFrame; private volatile ArrayList<Integer> mCallbacks = new ArrayList<Integer>(); private CallbackHelper mCallbackHelper = new CallbackHelper(); @@ -402,13 +419,13 @@ public class AwContentCaptureTest { ContentCaptureFrame c = data; Rect r = c.getBounds(); session.add(ContentCaptureFrame.createContentCaptureFrame( - c.getId(), c.getUrl(), r.left, r.top, r.width(), r.height(), null)); + c.getId(), c.getUrl(), r.left, r.top, r.width(), r.height(), null, null)); return session; } private FrameSession createFrameSession(String url) { FrameSession session = new FrameSession(1); - session.add(ContentCaptureFrame.createContentCaptureFrame(0, url, 0, 0, 0, 0, null)); + session.add(ContentCaptureFrame.createContentCaptureFrame(0, url, 0, 0, 0, 0, null, null)); return session; } @@ -830,4 +847,146 @@ public class AwContentCaptureTest { runScript("document.title='hello world'"); }, toIntArray(TestAwContentCaptureConsumer.TITLE_UPDATED)); } + + @Test + @SmallTest + @Feature({"AndroidWebView"}) + public void testFaviconRetrievedAtFirstContentCapture() throws Throwable { + // Starts with a empty document, so no content shall be streamed. + final String response = "<html><head>" + + "<link rel=\"apple-touch-icon\" href=\"image.png\">" + + "</head><body>" + + "<p id='place_holder'></p>" + + "</body></html>"; + final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null); + int count = mContentsClient.getTouchIconHelper().getCallCount(); + loadUrlSync(url); + // To simulate favicon being retrieved by WebContents before first Content is streamed, + // wait favicon being available in WebContents, then insert the text to document. + mContentsClient.getTouchIconHelper().waitForCallback(count); + Assert.assertEquals(1, mContentsClient.getTouchIconHelper().getTouchIconsCount()); + runAndVerifyCallbacks(() -> { + runScript("document.getElementById('place_holder').innerHTML = 'world';"); + }, toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED)); + GURL gurl = new GURL(url); + String origin = gurl.getOrigin().getSpec(); + // Blink attaches the default favicon if it is not specified in page. + final String expectedJson = String.format("[" + + " {" + + " \"type\" : \"favicon\"," + + " \"url\" : \"%sfavicon.ico\"" + + " }," + + " {" + + " \"type\" : \"touch icon\"," + + " \"url\" : \"%simage.png\"" + + " }" + + "]", + origin, origin); + verifyFaviconResult(expectedJson, mConsumer.getCapturedContent().getFavicon()); + } + + @Test + @SmallTest + @Feature({"AndroidWebView"}) + public void testFaviconRetrievedAfterFirstContentCapture() throws Throwable { + final String response = "<html><head'>" + + "</head><body>" + + "<p id='place_holder'>world</p>" + + "</body></html>"; + final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null); + // Direct ContentCaptureReveiver and OnscreenContentProvider not to get the favicon + // from Webontents, because there is no way to control the time of favicon update. + TestThreadUtils.runOnUiThreadBlocking( + () -> { ContentCaptureTestSupport.disableGetFaviconFromWebContents(); }); + runAndVerifyCallbacks(() -> { + loadUrlSync(url); + }, toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED)); + GURL gurl = new GURL(url); + String origin = gurl.getOrigin().getSpec(); + final String expectedJson = String.format("[" + + " {" + + " \"type\" : \"favicon\"," + + " \"url\" : \"%sfavicon.ico\"" + + " }," + + " {" + + " \"type\" : \"touch icon\"," + + " \"url\" : \"%simage.png\"" + + " }" + + "]", + origin, origin); + // Simulates favicon update by calling OnscreenContentProvider's test method. + runAndVerifyCallbacks(() -> { + TestThreadUtils.runOnUiThreadBlocking(() -> { + ContentCaptureTestSupport.simulateDidUpdateFaviconURL( + mAwContents.getWebContents(), expectedJson); + }); + }, toIntArray(TestAwContentCaptureConsumer.FAVICON_UPDATED)); + verifyFaviconResult(expectedJson, mConsumer.getFaviconUpdatedFrame().getFavicon()); + } + + @Test + @SmallTest + @Feature({"AndroidWebView"}) + public void testFavicon() throws Throwable { + final String response = "<html><head>" + + "<link rel=icon href=mac.icns sizes=\"128x128 512x512 8192x8192 32768x32768\">" + + "</head><body>" + + "<p>world</p>" + + "</body></html>"; + final String url = mWebServer.setResponse(MAIN_FRAME_FILE, response, null); + + runAndVerifyCallbacks(() -> { + loadUrlSync(url); + }, toIntArray(TestAwContentCaptureConsumer.CONTENT_CAPTURED)); + Long frameId = null; + Set<Long> capturedContentIds = null; + // Verify only on-screen content is captured. + verifyCapturedContent(null, frameId, url, null, toStringSet("world"), capturedContentIds, + mConsumer.getParentFrame(), mConsumer.getCapturedContent()); + // The favicon could be from either first capture or FaviconUpdated callback. + String favicon = mConsumer.getCapturedContent().getFavicon(); + if (favicon == null) { + // Update the title and verify the result. + runAndVerifyCallbacks( + () -> {}, toIntArray(TestAwContentCaptureConsumer.FAVICON_UPDATED)); + favicon = mConsumer.getFaviconUpdatedFrame().getFavicon(); + } + GURL gurl = new GURL(url); + String origin = gurl.getOrigin().getSpec(); + final String expectedJson = String.format("[" + + " {" + + " \"sizes\" : " + + " [" + + " {" + + " \"height\" : 128," + + " \"width\" : 128" + + " }," + + " {" + + " \"height\" : 512," + + " \"width\" : 512" + + " }," + + " {" + + " \"height\" : 8192," + + " \"width\" : 8192" + + " }," + + " {" + + " \"height\" : 32768," + + " \"width\" : 32768" + + " }" + + " ]," + + " \"type\" : \"favicon\"," + + " \"url\" : \"%smac.icns\"" + + " }" + + " ]", + origin); + verifyFaviconResult(expectedJson, favicon); + } + + private static void verifyFaviconResult(String expectedJson, String resultJson) + throws Throwable { + JSONArray expectedResult = (JSONArray) new JSONTokener(expectedJson).nextValue(); + JSONArray actualResult = (JSONArray) new JSONTokener(resultJson).nextValue(); + Assert.assertEquals(String.format("Actual:%s\n Expected:\n%s\n", resultJson, expectedJson), + expectedResult.toString(), actualResult.toString()); + } } diff --git a/android_webview/test/BUILD.gn b/android_webview/test/BUILD.gn index 47363125effa4..e3f95de613f67 100644 --- a/android_webview/test/BUILD.gn +++ b/android_webview/test/BUILD.gn @@ -60,6 +60,7 @@ android_apk("webview_instrumentation_apk") { "//base:base_java", "//base:base_java_test_support", "//components/android_autofill/browser/test_support:component_autofill_provider_java_test_support", + "//components/content_capture/android/test_support:java", "//components/embedder_support/android:util_java", "//components/heap_profiling/multi_process:heap_profiling_java_test_support", "//components/policy/android:policy_java_test_support", @@ -162,6 +163,7 @@ shared_library("libstandalonelibwebviewchromium") { "//android_webview/public", "//base", "//components/android_autofill/browser/test_support:component_autofill_provider_native_test_support", + "//components/content_capture/android/test_support", "//components/heap_profiling/multi_process:test_support", "//content/public/test/android:content_native_test_support", "//gpu/vulkan", @@ -202,6 +204,7 @@ instrumentation_test_apk("webview_instrumentation_test_apk") { "//components/component_updater/android:component_provider_service_aidl_java", "//components/component_updater/android:embedded_component_loader_java", "//components/content_capture/android:java", + "//components/content_capture/android/test_support:java", "//components/embedder_support/android:util_java", "//components/embedder_support/android:web_contents_delegate_java", "//components/heap_profiling/multi_process:heap_profiling_java_test_support", diff --git a/components/content_capture/android/DEPS b/components/content_capture/android/DEPS index 9e21581717da5..a0fec6f0900fe 100644 --- a/components/content_capture/android/DEPS +++ b/components/content_capture/android/DEPS @@ -2,5 +2,6 @@ include_rules = [ "+components/content_capture/android/jni_headers", "+content/public/android", "+content/public/browser", + "+third_party/blink/public/mojom/favicon", "+third_party/re2", -] \ No newline at end of file +] diff --git a/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureConsumer.java b/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureConsumer.java index d9729917a6bc1..8edf08e1f25f6 100644 --- a/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureConsumer.java +++ b/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureConsumer.java @@ -44,6 +44,12 @@ public interface ContentCaptureConsumer { */ void onTitleUpdated(ContentCaptureFrame mainFrame); + /** + * Invoked when the favicon is updated. + * @param mainFrame the frame whose favicon is updated. + */ + void onFaviconUpdated(ContentCaptureFrame mainFrame); + /** * @param urls * @return if the urls shall be captured. diff --git a/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureFrame.java b/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureFrame.java index b26e08d57d750..6d73875b75a0d 100644 --- a/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureFrame.java +++ b/components/content_capture/android/java/src/org/chromium/components/content_capture/ContentCaptureFrame.java @@ -16,19 +16,21 @@ import org.chromium.base.annotations.CalledByNative; public class ContentCaptureFrame extends ContentCaptureDataBase { private final String mUrl; private final String mTitle; + private final String mFavicon; @CalledByNative @VisibleForTesting - public static ContentCaptureFrame createContentCaptureFrame( - long id, String value, int x, int y, int width, int height, String title) { - return new ContentCaptureFrame(id, value, x, y, width, height, title); + public static ContentCaptureFrame createContentCaptureFrame(long id, String value, int x, int y, + int width, int height, String title, String favicon) { + return new ContentCaptureFrame(id, value, x, y, width, height, title, favicon); } - private ContentCaptureFrame( - long id, String value, int x, int y, int width, int height, String title) { + private ContentCaptureFrame(long id, String value, int x, int y, int width, int height, + String title, String favicon) { super(id, new Rect(x, y, x + width, y + height)); mUrl = value; mTitle = title; + mFavicon = favicon; } public String getUrl() { @@ -39,6 +41,10 @@ public class ContentCaptureFrame extends ContentCaptureDataBase { return mTitle; } + public String getFavicon() { + return mFavicon; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(super.toString()); diff --git a/components/content_capture/android/java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java b/components/content_capture/android/java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java index d5041253436ed..9b56128ebcbf3 100644 --- a/components/content_capture/android/java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java +++ b/components/content_capture/android/java/src/org/chromium/components/content_capture/ExperimentContentCaptureConsumer.java @@ -43,6 +43,11 @@ public class ExperimentContentCaptureConsumer implements ContentCaptureConsumer if (sDump) Log.d(TAG, "onTitleUpdated"); } + @Override + public void onFaviconUpdated(ContentCaptureFrame mainFrame) { + if (sDump) Log.d(TAG, "onFaviconUpdated"); + } + @Override public boolean shouldCapture(String[] urls) { return true; diff --git a/components/content_capture/android/java/src/org/chromium/components/content_capture/OnscreenContentProvider.java b/components/content_capture/android/java/src/org/chromium/components/content_capture/OnscreenContentProvider.java index a3896bef7d4de..b9d8f0c00292a 100644 --- a/components/content_capture/android/java/src/org/chromium/components/content_capture/OnscreenContentProvider.java +++ b/components/content_capture/android/java/src/org/chromium/components/content_capture/OnscreenContentProvider.java @@ -156,7 +156,18 @@ public class OnscreenContentProvider { consumer.onTitleUpdated(mainFrame); } } - if (sDump.booleanValue()) Log.i(TAG, "Updated Title: %s", mainFrame); + if (sDump.booleanValue()) Log.i(TAG, "Updated Title: %s", mainFrame.getTitle()); + } + + @CalledByNative + private void didUpdateFavicon(ContentCaptureFrame mainFrame) { + String[] urls = buildUrls(null, mainFrame); + for (ContentCaptureConsumer consumer : mContentCaptureConsumers) { + if (consumer.shouldCapture(urls)) { + consumer.onFaviconUpdated(mainFrame); + } + } + if (sDump.booleanValue()) Log.i(TAG, "Updated Favicon: %s", mainFrame.getFavicon()); } @CalledByNative diff --git a/components/content_capture/android/java/src/org/chromium/components/content_capture/PlatformContentCaptureConsumer.java b/components/content_capture/android/java/src/org/chromium/components/content_capture/PlatformContentCaptureConsumer.java index 3f2e1a68d6d25..373d68b69fc4b 100644 --- a/components/content_capture/android/java/src/org/chromium/components/content_capture/PlatformContentCaptureConsumer.java +++ b/components/content_capture/android/java/src/org/chromium/components/content_capture/PlatformContentCaptureConsumer.java @@ -82,6 +82,9 @@ public class PlatformContentCaptureConsumer implements ContentCaptureConsumer { .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } + @Override + public void onFaviconUpdated(ContentCaptureFrame mainFrame) {} + @Override public void onContentRemoved(FrameSession frame, long[] removedIds) { if (frame.isEmpty() || mPlatformSession == null) return; diff --git a/components/content_capture/android/junit/src/org/chromium/components/content_capture/PlatformAPIWrapperTest.java b/components/content_capture/android/junit/src/org/chromium/components/content_capture/PlatformAPIWrapperTest.java index 6203847e27114..23eaca16092a4 100644 --- a/components/content_capture/android/junit/src/org/chromium/components/content_capture/PlatformAPIWrapperTest.java +++ b/components/content_capture/android/junit/src/org/chromium/components/content_capture/PlatformAPIWrapperTest.java @@ -291,7 +291,7 @@ public class PlatformAPIWrapperTest { FrameSession frameSession = new FrameSession(1); frameSession.add(ContentCaptureFrame.createContentCaptureFrame(MAIN_ID, MAIN_URL, MAIN_FRAME_RECT.left, MAIN_FRAME_RECT.top, MAIN_FRAME_RECT.width(), - MAIN_FRAME_RECT.height(), MAIN_TITLE)); + MAIN_FRAME_RECT.height(), MAIN_TITLE, null)); return frameSession; } @@ -300,7 +300,7 @@ public class PlatformAPIWrapperTest { frameSessionForRemoveTask.add(0, ContentCaptureFrame.createContentCaptureFrame(CHILD_FRAME_ID, CHILD_URL, CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top, CHILD_FRAME_RECT.width(), - CHILD_FRAME_RECT.height(), CHILD_TITLE)); + CHILD_FRAME_RECT.height(), CHILD_TITLE, null)); return frameSessionForRemoveTask; } @@ -309,7 +309,7 @@ public class PlatformAPIWrapperTest { FrameSession frameSession = createFrameSession(); ContentCaptureFrame data = ContentCaptureFrame.createContentCaptureFrame(CHILD_FRAME_ID, CHILD_URL, CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top, CHILD_FRAME_RECT.width(), - CHILD_FRAME_RECT.height(), CHILD_TITLE); + CHILD_FRAME_RECT.height(), CHILD_TITLE, null); ContentCaptureData.createContentCaptureData(data, CHILD1_ID, CHILD1_TEXT, CHILD1_RECT.left, CHILD1_RECT.top, CHILD1_RECT.width(), CHILD1_RECT.height()); ContentCaptureData.createContentCaptureData(data, CHILD2_ID, CHILD2_TEXT, CHILD2_RECT.left, @@ -321,7 +321,7 @@ public class PlatformAPIWrapperTest { // Modifies child2 ContentCaptureFrame changeTextData = ContentCaptureFrame.createContentCaptureFrame( CHILD_FRAME_ID, CHILD_URL, CHILD_FRAME_RECT.left, CHILD_FRAME_RECT.top, - CHILD_FRAME_RECT.width(), CHILD_FRAME_RECT.height(), CHILD_TITLE); + CHILD_FRAME_RECT.width(), CHILD_FRAME_RECT.height(), CHILD_TITLE, null); ContentCaptureData.createContentCaptureData(changeTextData, CHILD2_ID, CHILD2_NEW_TEXT, CHILD2_RECT.left, CHILD2_RECT.top, CHILD2_RECT.width(), CHILD2_RECT.height()); return new ContentUpdateTask(createFrameSession(), changeTextData, mRootPlatformSession); @@ -340,7 +340,7 @@ public class PlatformAPIWrapperTest { private TitleUpdateTask createTitleUpdateTask() { ContentCaptureFrame mainFrame = ContentCaptureFrame.createContentCaptureFrame(MAIN_ID, MAIN_URL, MAIN_FRAME_RECT.left, MAIN_FRAME_RECT.top, MAIN_FRAME_RECT.width(), - MAIN_FRAME_RECT.height(), UPDATED_MAIN_TITLE); + MAIN_FRAME_RECT.height(), UPDATED_MAIN_TITLE, null); return new TitleUpdateTask(mainFrame, mRootPlatformSession); } diff --git a/components/content_capture/android/onscreen_content_provider_android.cc b/components/content_capture/android/onscreen_content_provider_android.cc index f0cae24102853..d56fc0355d439 100644 --- a/components/content_capture/android/onscreen_content_provider_android.cc +++ b/components/content_capture/android/onscreen_content_provider_android.cc @@ -55,10 +55,14 @@ ScopedJavaLocalRef<jobject> ToJavaObjectOfContentCaptureFrame( if (!data.title.empty()) jtitle = ConvertUTF16ToJavaString(env, data.title); + ScopedJavaLocalRef<jstring> jfavicon; + if (!data.favicon.empty()) + jfavicon = ConvertUTF8ToJavaString(env, data.favicon); + ScopedJavaLocalRef<jobject> jdata = Java_ContentCaptureFrame_createContentCaptureFrame( env, data.id, jurl, data.bounds.x(), data.bounds.y() + offset_y, - data.bounds.width(), data.bounds.height(), jtitle); + data.bounds.width(), data.bounds.height(), jtitle, jfavicon); if (jdata.is_null()) return jdata; for (const auto& child : data.children) { @@ -190,6 +194,22 @@ void OnscreenContentProviderAndroid::DidUpdateTitle( Java_OnscreenContentProvider_didUpdateTitle(env, java_ref_, jdata); } +void OnscreenContentProviderAndroid::DidUpdateFavicon( + const ContentCaptureFrame& main_frame) { + JNIEnv* env = AttachCurrentThread(); + DCHECK(java_ref_.obj()); + + auto* web_contents = GetWebContents(); + DCHECK(web_contents); + const int offset_y = Java_OnscreenContentProvider_getOffsetY( + env, java_ref_, web_contents->GetJavaWebContents()); + ScopedJavaLocalRef<jobject> jdata = + ToJavaObjectOfContentCaptureFrame(env, main_frame, offset_y); + if (jdata.is_null()) + return; + Java_OnscreenContentProvider_didUpdateFavicon(env, java_ref_, jdata); +} + bool OnscreenContentProviderAndroid::ShouldCapture(const GURL& url) { // Capture all urls for experiment, the url will be checked // before the content is sent to the consumers. diff --git a/components/content_capture/android/onscreen_content_provider_android.h b/components/content_capture/android/onscreen_content_provider_android.h index 686b32ad4ccf2..586f161ad5426 100644 --- a/components/content_capture/android/onscreen_content_provider_android.h +++ b/components/content_capture/android/onscreen_content_provider_android.h @@ -32,6 +32,7 @@ class OnscreenContentProviderAndroid : public ContentCaptureConsumer { const std::vector<int64_t>& data) override; void DidRemoveSession(const ContentCaptureSession& session) override; void DidUpdateTitle(const ContentCaptureFrame& main_frame) override; + void DidUpdateFavicon(const ContentCaptureFrame& main_frame) override; bool ShouldCapture(const GURL& url) override; base::android::ScopedJavaLocalRef<jobject> GetJavaObject(); diff --git a/components/content_capture/android/test_support/BUILD.gn b/components/content_capture/android/test_support/BUILD.gn new file mode 100644 index 0000000000000..1e6eec4fd04e5 --- /dev/null +++ b/components/content_capture/android/test_support/BUILD.gn @@ -0,0 +1,30 @@ +# Copyright 2021 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. + +import("//build/config/android/config.gni") +import("//build/config/android/rules.gni") + +testonly = true + +source_set("test_support") { + sources = [ "content_capture_test_support_android.cc" ] + deps = [ + ":jni_headers", + "//components/content_capture/browser", + ] +} + +android_library("java") { + deps = [ + "//base:base_java", + "//content/public/android:content_java", + "//third_party/androidx:androidx_annotation_annotation_java", + ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] + sources = [ "java/src/org/chromium/components/content_capture/ContentCaptureTestSupport.java" ] +} + +generate_jni("jni_headers") { + sources = [ "java/src/org/chromium/components/content_capture/ContentCaptureTestSupport.java" ] +} diff --git a/components/content_capture/android/test_support/content_capture_test_support_android.cc b/components/content_capture/android/test_support/content_capture_test_support_android.cc new file mode 100644 index 0000000000000..3fbd2291205d3 --- /dev/null +++ b/components/content_capture/android/test_support/content_capture_test_support_android.cc @@ -0,0 +1,76 @@ +// Copyright 2021 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 "components/content_capture/android/test_support/jni_headers/ContentCaptureTestSupport_jni.h" + +#include "base/android/jni_string.h" +#include "base/json/json_reader.h" +#include "base/notreached.h" +#include "base/values.h" +#include "components/content_capture/browser/content_capture_receiver.h" +#include "components/content_capture/browser/onscreen_content_provider.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h" +#include "ui/gfx/geometry/size.h" + +namespace content_capture { + +namespace { +blink::mojom::FaviconIconType ToType(std::string type) { + if (type == "favicon") + return blink::mojom::FaviconIconType::kFavicon; + else if (type == "touch icon") + return blink::mojom::FaviconIconType::kTouchIcon; + else if (type == "touch precomposed icon") + return blink::mojom::FaviconIconType::kTouchPrecomposedIcon; + NOTREACHED(); + return blink::mojom::FaviconIconType::kInvalid; +} + +} // namespace + +static void JNI_ContentCaptureTestSupport_DisableGetFaviconFromWebContents( + JNIEnv* env) { + ContentCaptureReceiver::DisableGetFaviconFromWebContentsForTesting(); +} + +static void JNI_ContentCaptureTestSupport_SimulateDidUpdateFaviconURL( + JNIEnv* env, + const base::android::JavaParamRef<jobject>& jwebContents, + const base::android::JavaParamRef<jstring>& jfaviconJson) { + content::WebContents* web_contents = + content::WebContents::FromJavaWebContents(jwebContents); + CHECK(web_contents); + OnscreenContentProvider* provider = + OnscreenContentProvider::FromWebContents(web_contents); + CHECK(provider); + + std::string json = base::android::ConvertJavaStringToUTF8(env, jfaviconJson); + absl::optional<base::Value> root = base::JSONReader::Read(json); + CHECK(root); + CHECK(root->is_list()); + std::vector<blink::mojom::FaviconURLPtr> favicon_urls; + for (const base::Value& icon : root->GetList()) { + std::vector<gfx::Size> sizes; + // The sizes is optional. + if (auto* icon_sizes = icon.FindKey("sizes")) { + for (const base::Value& size : icon_sizes->GetList()) { + CHECK(size.FindKey("width")); + CHECK(size.FindKey("height")); + sizes.emplace_back(size.FindKey("width")->GetInt(), + size.FindKey("height")->GetInt()); + } + } + CHECK(icon.FindKey("url")); + CHECK(icon.FindKey("type")); + favicon_urls.push_back(blink::mojom::FaviconURL::New( + GURL(*icon.FindKey("url")->GetIfString()), + ToType(*icon.FindKey("type")->GetIfString()), sizes)); + } + CHECK(!favicon_urls.empty()); + provider->NotifyFaviconURLUpdatedForTesting(web_contents->GetMainFrame(), + favicon_urls); +} + +} // namespace content_capture diff --git a/components/content_capture/android/test_support/java/src/org/chromium/components/content_capture/ContentCaptureTestSupport.java b/components/content_capture/android/test_support/java/src/org/chromium/components/content_capture/ContentCaptureTestSupport.java new file mode 100644 index 0000000000000..602100a74c802 --- /dev/null +++ b/components/content_capture/android/test_support/java/src/org/chromium/components/content_capture/ContentCaptureTestSupport.java @@ -0,0 +1,28 @@ +// Copyright 2021 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.components.content_capture; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.content_public.browser.WebContents; + +/** + * This is the test support class to help setup various test conditions. + */ +@JNINamespace("content_capture") +public class ContentCaptureTestSupport { + public static void disableGetFaviconFromWebContents() { + ContentCaptureTestSupportJni.get().disableGetFaviconFromWebContents(); + } + + public static void simulateDidUpdateFaviconURL(WebContents webContents, String faviconJson) { + ContentCaptureTestSupportJni.get().simulateDidUpdateFaviconURL(webContents, faviconJson); + } + + @NativeMethods + interface Natives { + void disableGetFaviconFromWebContents(); + void simulateDidUpdateFaviconURL(WebContents webContents, String faviconJson); + } +} diff --git a/components/content_capture/browser/content_capture_consumer.h b/components/content_capture/browser/content_capture_consumer.h index 9cf8b44fcce84..a21194f322b51 100644 --- a/components/content_capture/browser/content_capture_consumer.h +++ b/components/content_capture/browser/content_capture_consumer.h @@ -55,6 +55,8 @@ class ContentCaptureConsumer { virtual void DidRemoveSession(const ContentCaptureSession& session) = 0; // Invoked when the given |main_frame|'s title updated. virtual void DidUpdateTitle(const ContentCaptureFrame& main_frame) = 0; + // Invoked when the given |main_frame|'s favicon updated. + virtual void DidUpdateFavicon(const ContentCaptureFrame& main_frame) = 0; // Return if the |url| shall be captured. Even return false, the content might // still be streamed because of the other consumers require it. Consumer can diff --git a/components/content_capture/browser/content_capture_receiver.cc b/components/content_capture/browser/content_capture_receiver.cc index 4e5ac095e8d73..b9c6af181a420 100644 --- a/components/content_capture/browser/content_capture_receiver.cc +++ b/components/content_capture/browser/content_capture_receiver.cc @@ -216,10 +216,15 @@ void ContentCaptureReceiver::UpdateFaviconURL( if (!has_session_) return; frame_content_capture_data_.favicon = ToJSON(candidates); + auto* provider = GetOnscreenContentProvider(rfh_); + if (!provider) + return; + provider->DidUpdateFavicon(this); } void ContentCaptureReceiver::RetrieveFaviconURL() { - if (!rfh()->IsActive() || rfh()->GetMainFrame() != rfh()) { + if (!rfh()->IsActive() || rfh()->GetMainFrame() != rfh() || + disable_get_favicon_from_web_contents_for_testing()) { frame_content_capture_data_.favicon = std::string(); } else { frame_content_capture_data_.favicon = ToJSON( @@ -267,4 +272,19 @@ const ContentCaptureFrame& ContentCaptureReceiver::GetContentCaptureFrame() { return frame_content_capture_data_; } +// static +bool + ContentCaptureReceiver::disable_get_favicon_from_web_contents_for_testing_ = + false; + +void ContentCaptureReceiver::DisableGetFaviconFromWebContentsForTesting() { + disable_get_favicon_from_web_contents_for_testing_ = true; +} + +// static +bool ContentCaptureReceiver:: + disable_get_favicon_from_web_contents_for_testing() { + return disable_get_favicon_from_web_contents_for_testing_; +} + } // namespace content_capture diff --git a/components/content_capture/browser/content_capture_receiver.h b/components/content_capture/browser/content_capture_receiver.h index 01398a8e03143..914fe9012558b 100644 --- a/components/content_capture/browser/content_capture_receiver.h +++ b/components/content_capture/browser/content_capture_receiver.h @@ -59,6 +59,9 @@ class ContentCaptureReceiver : public mojom::ContentCaptureReceiver { void UpdateFaviconURL( const std::vector<blink::mojom::FaviconURLPtr>& candidates); + static void DisableGetFaviconFromWebContentsForTesting(); + static bool disable_get_favicon_from_web_contents_for_testing(); + private: FRIEND_TEST_ALL_PREFIXES(ContentCaptureReceiverTest, RenderFrameHostGone); FRIEND_TEST_ALL_PREFIXES(ContentCaptureReceiverTest, TitleUpdateTaskDelay); @@ -104,6 +107,8 @@ class ContentCaptureReceiver : public mojom::ContentCaptureReceiver { // prevent running frequently. unsigned exponential_delay_ = 1; + static bool disable_get_favicon_from_web_contents_for_testing_; + mojo::AssociatedRemote<mojom::ContentCaptureSender> content_capture_sender_; DISALLOW_COPY_AND_ASSIGN(ContentCaptureReceiver); }; diff --git a/components/content_capture/browser/content_capture_receiver_test.cc b/components/content_capture/browser/content_capture_receiver_test.cc index 59c1606130207..4411a1088d72f 100644 --- a/components/content_capture/browser/content_capture_receiver_test.cc +++ b/components/content_capture/browser/content_capture_receiver_test.cc @@ -113,6 +113,8 @@ class ContentCaptureConsumerHelper : public ContentCaptureConsumer { updated_title_ = main_frame.title; } + void DidUpdateFavicon(const ContentCaptureFrame& main_frame) override {} + bool ShouldCapture(const GURL& url) override { return false; } const ContentCaptureSession& parent_session() const { diff --git a/components/content_capture/browser/onscreen_content_provider.cc b/components/content_capture/browser/onscreen_content_provider.cc index 038ea01f777a0..1cea5371e28c2 100644 --- a/components/content_capture/browser/onscreen_content_provider.cc +++ b/components/content_capture/browser/onscreen_content_provider.cc @@ -217,6 +217,16 @@ void OnscreenContentProvider::DidUpdateTitle( void OnscreenContentProvider::DidUpdateFaviconURL( content::RenderFrameHost* render_frame_host, const std::vector<blink::mojom::FaviconURLPtr>& candidates) { + if (ContentCaptureReceiver:: + disable_get_favicon_from_web_contents_for_testing()) { + return; + } + NotifyFaviconURLUpdated(render_frame_host, candidates); +} + +void OnscreenContentProvider::NotifyFaviconURLUpdated( + content::RenderFrameHost* render_frame_host, + const std::vector<blink::mojom::FaviconURLPtr>& candidates) { // Only set the favicons for the mainframe. if (render_frame_host != web_contents()->GetMainFrame()) return; @@ -226,6 +236,18 @@ void OnscreenContentProvider::DidUpdateFaviconURL( } } +void OnscreenContentProvider::DidUpdateFavicon( + ContentCaptureReceiver* content_capture_receiver) { + ContentCaptureSession session; + BuildContentCaptureSession(content_capture_receiver, + /*ancestor_only=*/false, &session); + + // Shall only update mainframe's title. + DCHECK(session.size() == 1); + for (auto* consumer : consumers_) + consumer->DidUpdateFavicon(*session.begin()); +} + void OnscreenContentProvider::BuildContentCaptureSession( ContentCaptureReceiver* content_capture_receiver, bool ancestor_only, diff --git a/components/content_capture/browser/onscreen_content_provider.h b/components/content_capture/browser/onscreen_content_provider.h index 0887001a37603..e29753fdb9b6d 100644 --- a/components/content_capture/browser/onscreen_content_provider.h +++ b/components/content_capture/browser/onscreen_content_provider.h @@ -58,6 +58,7 @@ class OnscreenContentProvider : public content::WebContentsObserver, const std::vector<int64_t>& data); void DidRemoveSession(ContentCaptureReceiver* content_capture_receiver); void DidUpdateTitle(ContentCaptureReceiver* content_capture_receiver); + void DidUpdateFavicon(ContentCaptureReceiver* content_capture_receiver); // content::WebContentsObserver: void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override; @@ -75,6 +76,12 @@ class OnscreenContentProvider : public content::WebContentsObserver, return weak_ptr_factory_.GetWeakPtr(); } + void NotifyFaviconURLUpdatedForTesting( + content::RenderFrameHost* render_frame_host, + const std::vector<blink::mojom::FaviconURLPtr>& candidates) { + NotifyFaviconURLUpdated(render_frame_host, candidates); + } + #ifdef UNIT_TEST ContentCaptureReceiver* ContentCaptureReceiverForFrameForTesting( content::RenderFrameHost* render_frame_host) const { @@ -110,6 +117,10 @@ class OnscreenContentProvider : public content::WebContentsObserver, bool ShouldCapture(const GURL& url); + void NotifyFaviconURLUpdated( + content::RenderFrameHost* render_frame_host, + const std::vector<blink::mojom::FaviconURLPtr>& candidates); + std::map<content::RenderFrameHost*, std::unique_ptr<ContentCaptureReceiver>> frame_map_; diff --git a/weblayer/browser/java/org/chromium/weblayer_private/test/TestContentCaptureConsumer.java b/weblayer/browser/java/org/chromium/weblayer_private/test/TestContentCaptureConsumer.java index 7cf1981e4e129..a4947dda3fcae 100644 --- a/weblayer/browser/java/org/chromium/weblayer_private/test/TestContentCaptureConsumer.java +++ b/weblayer/browser/java/org/chromium/weblayer_private/test/TestContentCaptureConsumer.java @@ -19,6 +19,7 @@ public class TestContentCaptureConsumer implements ContentCaptureConsumer { public static final int CONTENT_REMOVED = 3; public static final int SESSION_REMOVED = 4; public static final int TITLE_UPDATED = 5; + public static final int FAVICON_UPDATED = 6; public TestContentCaptureConsumer(Runnable onNewEvents, ArrayList<Integer> eventsObserved) { mOnNewEvents = onNewEvents; @@ -56,6 +57,12 @@ public class TestContentCaptureConsumer implements ContentCaptureConsumer { mOnNewEvents.run(); } + @Override + public void onFaviconUpdated(ContentCaptureFrame mainFrame) { + mEventsObserved.add(FAVICON_UPDATED); + mOnNewEvents.run(); + } + @Override public boolean shouldCapture(String[] urls) { return true;