Commit 06c5dc33 authored by Ben Goldberger's avatar Ben Goldberger Committed by Commit Bot

Add C++ code and JNI interface to support context fetching with javascript.

Also
-Add histogram to track js execution time.
-Pipe web contents into internal code to allow for triggering this module from the lens sdk.

Bug: 1145865
Change-Id: Ia22e285d7b8ebcf6fa214dcb7d8aa35aa7202c89
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2520334Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarBen Goldberger <benwgold@google.com>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Commit-Queue: Ben Goldberger <benwgold@google.com>
Auto-Submit: Ben Goldberger <benwgold@google.com>
Cr-Commit-Position: refs/heads/master@{#826403}
parent 387a97ab
......@@ -3271,6 +3271,7 @@ generate_jni("chrome_jni_headers") {
"java/src/org/chromium/chrome/browser/installedapp/InstalledAppProviderImpl.java",
"java/src/org/chromium/chrome/browser/installedapp/PackageHash.java",
"java/src/org/chromium/chrome/browser/instantapps/InstantAppsSettings.java",
"java/src/org/chromium/chrome/browser/javascript/WebContextFetcher.java",
"java/src/org/chromium/chrome/browser/locale/LocaleManager.java",
"java/src/org/chromium/chrome/browser/locale/LocaleTemplateUrlLoader.java",
"java/src/org/chromium/chrome/browser/login/ChromeHttpAuthHandler.java",
......
......@@ -859,6 +859,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/invalidation/ChromeBrowserSyncAdapterServiceImpl.java",
"java/src/org/chromium/chrome/browser/invalidation/ResumableDelayedTaskRunner.java",
"java/src/org/chromium/chrome/browser/invalidation/SessionsInvalidationManager.java",
"java/src/org/chromium/chrome/browser/javascript/WebContextFetcher.java",
"java/src/org/chromium/chrome/browser/language/AppLocaleUtils.java",
"java/src/org/chromium/chrome/browser/language/GlobalAppLocaleController.java",
"java/src/org/chromium/chrome/browser/language/LanguageAskPrompt.java",
......
......@@ -162,7 +162,7 @@ public class ContextMenuHelper {
if (LensUtils.enableImageChip(mIsIncognito)
&& LensController.getInstance().isQueryEnabled()) {
LensAsyncManager lensAsyncManager = new LensAsyncManager(mCurrentContextMenuParams,
mCurrentNativeDelegate, mWindow, mIsIncognito, mPageTitle);
mCurrentNativeDelegate, mWindow, mIsIncognito, mPageTitle, mWebContents);
menuCoordinator.displayMenuWithLensChip(mWindow, mWebContents,
mCurrentContextMenuParams, items, mCallback, mOnMenuShown, mOnMenuClosed,
lensAsyncManager);
......
......@@ -12,6 +12,7 @@ import org.chromium.chrome.browser.lens.LensQueryParams;
import org.chromium.chrome.browser.lens.LensQueryResult;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
// TODO(b/170970926): Move LensAsyncManager to the private code repository.
......@@ -27,6 +28,7 @@ class LensAsyncManager {
private WindowAndroid mWindow;
private boolean mIsIncognito;
private String mPageTitle;
private WebContents mWebContents;
/**
* Construct a lens async manager.
......@@ -34,14 +36,17 @@ class LensAsyncManager {
* @param nativeDelegate {@link ContextMenuNativeDelegate} used to retrieve image bytes.
* @param window The current window.
* @param isIncognito Whether the current tab is in incognito mode.
* @param pageTitle The title of the current tab's document.
* @param webContents The web contents representing the selected element.
*/
public LensAsyncManager(ContextMenuParams params, ContextMenuNativeDelegate nativeDelegate,
WindowAndroid window, boolean isIncognito, String pageTitle) {
WindowAndroid window, boolean isIncognito, String pageTitle, WebContents webContents) {
mParams = params;
mNativeDelegate = nativeDelegate;
mWindow = window;
mIsIncognito = isIncognito;
mPageTitle = pageTitle;
mWebContents = webContents;
}
/**
......@@ -56,6 +61,7 @@ class LensAsyncManager {
.withPageUrl(mParams.getPageUrl())
.withImageTitleOrAltText(mParams.getTitleText())
.withPageTitle(mPageTitle)
.withWebContents(mWebContents)
.build();
LensController.getInstance().queryImage(lensQueryParams, (lensQueryResult) -> {
mLastCompletedQueryResult = lensQueryResult;
......
// Copyright 2020 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.chrome.browser.javascript;
import android.util.JsonReader;
import android.util.JsonToken;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.RenderFrameHost;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
/**
* Provides ability to fetch content from the document using Javascript.
*/
public class WebContextFetcher {
private static final String TAG = "WebContextFetcher";
/**
* A utility method which allows Java code to extract content from the page using
* a Javascript string. The script should be a self executing function returning
* a javascript dictionary object. The return value must be flat (so no nested fields)
* and must only contain string values (no integers or other objects) and will throw
* an assertion error otherwise.
* @param script The javascript string.
* @param callback The callback to execute after executing the JS and converting to a
* java map.
* @param renderFrameHost The frame to execute the JS on.
*
*/
public static void fetchContextWithJavascript(String script,
Callback<Map<String, String>> callback, RenderFrameHost renderFrameHost) {
WebContextFetcherJni.get().fetchContextWithJavascript(script, (jsonString) -> {
callback.onResult(convertJsonToMap(jsonString));
}, renderFrameHost);
}
private static Map<String, String> convertJsonToMap(String jsonString) {
Map<String, String> fetchedContext = new HashMap<>();
try {
JsonReader jsonReader = new JsonReader(new StringReader(jsonString));
// The JSON should be an object and not an array.
if (jsonReader.peek() != JsonToken.BEGIN_OBJECT) {
throw new AssertionError("Error reading JSON object value.");
}
jsonReader.beginObject();
while (jsonReader.hasNext()) {
// The JSON object should be a flat key value map with non-null values.
// Otherwise fire an assertion error.
if (jsonReader.peek() != JsonToken.NAME) {
throw new AssertionError("Error reading JSON name value.");
}
String key = jsonReader.nextName();
if (jsonReader.peek() != JsonToken.STRING) {
throw new AssertionError("Error reading JSON string value.");
}
String value = jsonReader.nextString();
fetchedContext.put(key, value);
}
jsonReader.endObject();
} catch (IOException e) {
Log.e(TAG, "Failed to read web context json");
}
return fetchedContext;
}
@NativeMethods
interface Natives {
void fetchContextWithJavascript(
String script, Callback<String> callback, RenderFrameHost renderFrameHost);
}
}
......@@ -2686,6 +2686,7 @@ static_library("browser") {
"android/intent_handler.cc",
"android/intent_helper.cc",
"android/intent_helper.h",
"android/javascript/web_context_fetcher.cc",
"android/locale/locale_manager.cc",
"android/locale/locale_manager.h",
"android/locale/locale_template_url_loader.cc",
......
// Copyright 2020 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 "base/android/callback_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/android/chrome_jni_headers/WebContextFetcher_jni.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
using base::android::AttachCurrentThread;
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
// The JS execution function returns the JSON object as a quoted string literal.
// Remove the surrounding quotes and the internal escaping, to convert it into a
// JSON object that can be parsed. E.g.:
// "{\"foo\":\"bar\"}" --> {"foo":"bar"}
static std::string ConvertJavascriptOutputToValidJson(std::string& json) {
// Remove trailing and leading double quotation characters.
std::string trimmed_json = json.substr(1, json.size() - 2);
// Remove escape slash from before quotations.
std::string substring_to_search = "\\\"";
std::string replace_str = "\"";
size_t pos = trimmed_json.find(substring_to_search);
// Repeat till end is reached.
while (pos != std::string::npos) {
// Remove occurrence of the escape character before quotes.
trimmed_json.replace(pos, substring_to_search.size(), replace_str);
// Get the next occurrence from the current position.
pos = trimmed_json.find(substring_to_search, pos + replace_str.size());
}
return trimmed_json;
}
static void OnContextFetchComplete(
const ScopedJavaGlobalRef<jobject>& scoped_jcallback,
base::TimeTicks javascript_start,
base::Value result) {
if (!javascript_start.is_null()) {
base::TimeDelta javascript_time = base::TimeTicks::Now() - javascript_start;
// TODO(benwgold): Update so that different js scripts can monitor execution
// separately.
base::UmaHistogramTimes("WebContextFetcher.JavaScriptRunner.ExecutionTime",
javascript_time);
DVLOG(1) << "WebContextFetcher.JavaScriptRunner.ExecutionTime = "
<< javascript_time;
}
std::string json;
base::JSONWriter::Write(result, &json);
base::android::RunStringCallbackAndroid(
scoped_jcallback, ConvertJavascriptOutputToValidJson(json));
}
// IMPORTANT: The output of this fetch should only be handled in memory safe
// languages (Java) and should not be parsed in C++.
static void ExecuteFetch(const base::string16& script,
const ScopedJavaGlobalRef<jobject>& scoped_jcallback,
content::RenderFrameHost* render_frame_host) {
DCHECK(render_frame_host);
// TODO(benwgold): Consider adding handling for cases when the document is not
// yet ready.
base::OnceCallback<void(base::Value)> callback = base::BindOnce(
&OnContextFetchComplete, scoped_jcallback, base::TimeTicks::Now());
render_frame_host->ExecuteJavaScriptInIsolatedWorld(
script, std::move(callback), ISOLATED_WORLD_ID_CHROME_INTERNAL);
}
static void JNI_WebContextFetcher_FetchContextWithJavascript(
JNIEnv* env,
const JavaParamRef<jstring>& jscript,
const JavaParamRef<jobject>& jcallback,
const JavaParamRef<jobject>& jrender_frame_host) {
auto* render_frame_host =
content::RenderFrameHost::FromJavaRenderFrameHost(jrender_frame_host);
base::string16 script = base::android::ConvertJavaStringToUTF16(env, jscript);
ScopedJavaGlobalRef<jobject> scoped_jcallback(env, jcallback);
ExecuteFetch(script, scoped_jcallback, render_frame_host);
}
......@@ -16415,6 +16415,18 @@ regressions. -->
</summary>
</histogram>
<histogram name="WebContextFetcher.Time.RunJavaScript" units="ms"
expires_after="2021-11-01">
<owner>benwgold@chromium.org</owner>
<owner>mdjones@chromium.org</owner>
<summary>
Records the number of milliseconds that it takes for a web context fetch to
complete. Recorded when the javascript execution completes. To start will be
used for Google Lens image queries to track time to retrieve page context,
but may extend to other features over time.
</summary>
</histogram>
<histogram name="WebController.BackForwardListOutOfSync" enum="BooleanHit"
expires_after="2020-12-25">
<owner>ajuma@chromium.org</owner>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment