diff --git a/base/android/jni_android.cc b/base/android/jni_android.cc index 0ddd543a7396..c5e2acddbcb2 100644 --- a/base/android/jni_android.cc +++ b/base/android/jni_android.cc @@ -4,6 +4,7 @@ #include "base/android/jni_android.h" +#include #include #include @@ -13,6 +14,8 @@ #include "base/base_jni_headers/PiiElider_jni.h" #include "base/debug/debugging_buildflags.h" #include "base/logging.h" +#include "base/base_switches.h" +#include "base/command_line.h" #include "build/build_config.h" #include "third_party/abseil-cpp/absl/base/attributes.h" @@ -30,9 +33,48 @@ ABSL_CONST_INIT thread_local void* stack_frame_pointer = nullptr; bool g_fatal_exception_occurred = false; +#if BUILDFLAG(IS_COBALT) +const char* COBALT_ORG_CHROMIUM = "cobalt/org/chromium"; +const char* ORG_CHROMIUM = "org/chromium"; + +bool g_add_cobalt_prefix = false; +std::atomic g_checked_command_line(false); + +std::string getRepackagedName(const char* signature) { + std::string holder(signature); + size_t pos = 0; + while ((pos = holder.find(ORG_CHROMIUM, pos)) != std::string::npos) { + holder.replace(pos, strlen(ORG_CHROMIUM), COBALT_ORG_CHROMIUM); + pos += strlen(COBALT_ORG_CHROMIUM); + } + return holder; +} + +bool shouldAddCobaltPrefix() { + if (!g_checked_command_line && base::CommandLine::InitializedForCurrentProcess()) { + g_add_cobalt_prefix = base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kCobaltJniPrefix); + g_checked_command_line = true; + } + return g_add_cobalt_prefix; +} +#endif + ScopedJavaLocalRef GetClassInternal(JNIEnv* env, +#if BUILDFLAG(IS_COBALT) + const char* original_class_name, + jobject class_loader) { + const char* class_name; + std::string holder; + if (shouldAddCobaltPrefix()) { + holder = getRepackagedName(original_class_name); + class_name = holder.c_str(); + } else { + class_name = original_class_name; + } +#else const char* class_name, jobject class_loader) { +#endif jclass clazz; if (class_loader != nullptr) { // ClassLoader.loadClass expects a classname with components separated by @@ -229,7 +271,17 @@ jmethodID MethodID::LazyGet(JNIEnv* env, const jmethodID value = atomic_method_id->load(std::memory_order_acquire); if (value) return value; +#if BUILDFLAG(IS_COBALT) + jmethodID id; + if (shouldAddCobaltPrefix()) { + std::string holder = getRepackagedName(jni_signature); + id = MethodID::Get(env, clazz, method_name, holder.c_str()); + } else { + id = MethodID::Get(env, clazz, method_name, jni_signature); + } +#else jmethodID id = MethodID::Get(env, clazz, method_name, jni_signature); +#endif atomic_method_id->store(id, std::memory_order_release); return id; } diff --git a/base/base_switches.cc b/base/base_switches.cc index 8e0a4855d420..00c1f165b703 100644 --- a/base/base_switches.cc +++ b/base/base_switches.cc @@ -183,4 +183,8 @@ extern const char kEnableCrashpad[] = "enable-crashpad"; const char kSchedulerBoostUrgent[] = "scheduler-boost-urgent"; #endif +#if BUILDFLAG(IS_COBALT) +const char kCobaltJniPrefix[] = "cobalt-jni-prefix"; +#endif + } // namespace switches diff --git a/base/base_switches.h b/base/base_switches.h index cead11f7ef24..093ba75edb3a 100644 --- a/base/base_switches.h +++ b/base/base_switches.h @@ -67,6 +67,10 @@ extern const char kEnableCrashpad[]; extern const char kSchedulerBoostUrgent[]; #endif +#if BUILDFLAG(IS_COBALT) +extern const char kCobaltJniPrefix[]; +#endif + } // namespace switches #endif // BASE_BASE_SWITCHES_H_ diff --git a/cobalt/android/BUILD.gn b/cobalt/android/BUILD.gn index eb9b6249f6ae..231b39929fb3 100644 --- a/cobalt/android/BUILD.gn +++ b/cobalt/android/BUILD.gn @@ -15,6 +15,7 @@ android_resources("cobalt_java_resources") { "apk/app/src/app/res/mipmap-xxhdpi/ic_app.png", "apk/app/src/app/res/values/strings.xml", "apk/app/src/main/res/layout/coat_error_dialog.xml", + "apk/app/src/main/res/layout/content_shell_activity.xml", "apk/app/src/main/res/values/colors.xml", "apk/app/src/main/res/values/ids.xml", "apk/app/src/main/res/values/overlayable.xml", @@ -36,14 +37,77 @@ generate_jni("jni_headers") { sources = [ "apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java" ] } -# TODO(cobalt): Re-enable or remove disabled java files. -android_library("cobalt_apk_java") { +generate_jni("content_shell_jni_headers") { + sources = [ + "apk/app/src/org/chromium/content_shell/Shell.java", + "apk/app/src/org/chromium/content_shell/ShellManager.java", + ] +} + +android_library("content_shell_apk_java") { testonly = true - resources_package = "dev.cobalt.coat" + + resources_package = "org.chromium.content_shell_apk" + deps = [ + ":cobalt_shell_java", + "//base:base_java", + "//base:process_launcher_java", + "//build/android:build_java", + "//components/embedder_support/android:view_java", + "//content/public/android:content_java", + "//content/shell/android:content_shell_apk_resources", + "//content/shell/android:content_shell_manifest", + "//media/capture/video/android:capture_java", + "//net/android:net_java", + "//third_party/android_deps:com_google_code_findbugs_jsr305_java", + "//ui/android:ui_java", + "//url:gurl_java", + ] + + sources = [ + "//content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellActivity.java", + "//content/shell/android/shell_apk/src/org/chromium/content_shell_apk/ContentShellApplication.java", + ] +} + +android_library("cobalt_shell_java") { + testonly = true + resources_package = "org.chromium.content_shell" + deps = [ + ":cobalt_java_resources", + "//base:base_java", + "//base:jni_java", + "//build/android:build_java", + "//components/download/internal/common:internal_java", + "//components/embedder_support/android:content_view_java", + "//components/embedder_support/android:view_java", + "//components/viz/service:service_java", + "//content/public/android:content_java", + "//content/shell/android:content_shell_java_resources", + "//media/base/android:media_java", + "//media/capture/video/android:capture_java", + "//mojo/public/java:system_java", + "//net/android:net_java", + "//ui/android:ui_java", + "//ui/base/cursor/mojom:cursor_type_java", + "//url:gurl_java", + ] + sources = [ + "apk/app/src/org/chromium/content_shell/Shell.java", + "apk/app/src/org/chromium/content_shell/ShellManager.java", + "apk/app/src/org/chromium/content_shell/ShellViewAndroidDelegate.java", + ] + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} +android_library("cobalt_main_java") { + testonly = true + resources_package = "dev.cobalt.coat" deps = [ ":cobalt_java_resources", + ":cobalt_shell_java", + ":content_shell_apk_java", ":jni_headers", "//base:base_java", "//base:jni_java", @@ -52,9 +116,6 @@ android_library("cobalt_apk_java") { "//components/embedder_support/android:view_java", "//components/version_info/android:version_constants_java", "//content/public/android:content_java", - "//content/shell/android:content_shell_apk_java", - "//content/shell/android:content_shell_apk_resources", - "//content/shell/android:content_shell_java", "//content/shell/android:content_shell_manifest", "//media/capture/video/android:capture_java", "//net/android:net_java", @@ -65,10 +126,7 @@ android_library("cobalt_apk_java") { "//ui/android:ui_no_recycler_view_java", "//url:gurl_java", ] - sources = [ - "apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java", - "apk/app/src/app/java/dev/cobalt/app/MainActivity.java", "apk/app/src/main/java/dev/cobalt/coat/ArtworkDownloader.java", "apk/app/src/main/java/dev/cobalt/coat/ArtworkDownloaderDefault.java", "apk/app/src/main/java/dev/cobalt/coat/ArtworkLoader.java", @@ -120,6 +178,20 @@ android_library("cobalt_apk_java") { "apk/app/src/main/java/dev/cobalt/util/SystemPropertiesHelper.java", "apk/app/src/main/java/dev/cobalt/util/UsedByNative.java", ] + + annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] +} + +android_library("cobalt_apk_java") { + testonly = true + resources_package = "dev.cobalt.coat" + + deps = [ ":cobalt_main_java" ] + + sources = [ + "apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java", + "apk/app/src/app/java/dev/cobalt/app/MainActivity.java", + ] } android_assets("cobalt_apk_assets") { @@ -143,15 +215,15 @@ template("content_shell_apk_tmpl") { deps = [] } deps += [ + ":cobalt_shell_java", + ":content_shell_apk_java", "//base:base_java_test_support", "//components/crash/android:java", "//components/crash/core/app:chrome_crashpad_handler_named_as_so", "//components/metrics:metrics_java", "//content/public/android:content_java", "//content/public/test/android:android_test_message_pump_support_java", - "//content/shell/android:content_shell_apk_java", "//content/shell/android:content_shell_assets", - "//content/shell/android:content_shell_java", "//media/capture/video/android:capture_java", "//net/android:net_java", "//services/shape_detection:shape_detection_java", @@ -168,6 +240,7 @@ shared_library("libcobalt_content_shell_content_view") { # TODO(b/375655377): remove testonly testonly = true deps = [ + ":content_shell_jni_headers", "//cobalt/user_agent", # TODO: what can be removed in the dependencies? @@ -175,7 +248,6 @@ shared_library("libcobalt_content_shell_content_view") { "//content/shell:content_shell_app", "//content/shell:content_shell_lib", "//content/shell:pak", - "//content/shell/android:content_shell_jni_headers", "//media", "//skia", "//starboard/android/shared:starboard_jni_state", diff --git a/cobalt/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java b/cobalt/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java index 7bae30a55a94..6d683791d2a2 100644 --- a/cobalt/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java +++ b/cobalt/android/apk/app/src/app/java/dev/cobalt/app/CobaltApplication.java @@ -17,11 +17,17 @@ import android.app.Application; import android.content.Context; import dev.cobalt.coat.StarboardBridge; - -import org.chromium.content_shell_apk.ContentShellApplication; +import org.chromium.base.ApplicationStatus; +import org.chromium.base.ContextUtils; +import org.chromium.base.PathUtils; +import org.chromium.base.library_loader.LibraryLoader; +import org.chromium.base.library_loader.LibraryProcessType; +import org.chromium.ui.base.ResourceBundle; /** Android Application hosting the Starboard application. */ -public class CobaltApplication extends ContentShellApplication implements StarboardBridge.HostApplication { +public class CobaltApplication extends Application implements StarboardBridge.HostApplication { + private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "content_shell"; + StarboardBridge starboardBridge; @Override @@ -33,4 +39,19 @@ public void setStarboardBridge(StarboardBridge starboardBridge) { public StarboardBridge getStarboardBridge() { return starboardBridge; } + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + boolean isBrowserProcess = !ContextUtils.getProcessName().contains(":"); + ContextUtils.initApplicationContext(this); + ResourceBundle.setNoAvailableLocalePaks(); + LibraryLoader.getInstance().setLibraryProcessType(isBrowserProcess + ? LibraryProcessType.PROCESS_BROWSER + : LibraryProcessType.PROCESS_CHILD); + if (isBrowserProcess) { + PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); + ApplicationStatus.initialize(this); + } + } } diff --git a/cobalt/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java b/cobalt/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java index 357463046ec6..cd1d03172108 100644 --- a/cobalt/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java +++ b/cobalt/android/apk/app/src/app/java/dev/cobalt/app/MainActivity.java @@ -16,6 +16,7 @@ import android.app.Activity; import android.app.Service; +import android.os.Bundle; import dev.cobalt.coat.ArtworkDownloaderDefault; import dev.cobalt.coat.CobaltActivity; import dev.cobalt.coat.CobaltService; @@ -50,4 +51,10 @@ protected StarboardBridge createStarboardBridge(String[] args, String startDeepL return bridge; } + + @Override + public void onCreate(Bundle savedInstanceState) { + this.shouldSetJNIPrefix = false; + super.onCreate(savedInstanceState); + } } diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index 5b2b75e133b8..ca019dd7d4b6 100644 --- a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -32,7 +32,6 @@ import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.Toast; import androidx.annotation.Nullable; import dev.cobalt.coat.javabridge.CobaltJavaScriptAndroidObject; @@ -96,6 +95,7 @@ public abstract class CobaltActivity extends Activity { private Intent mLastSentIntent; private String mStartupUrl; private IntentRequestTracker mIntentRequestTracker; + protected Boolean shouldSetJNIPrefix = true; // Initially copied from ContentShellActiviy.java protected void createContent(final Bundle savedInstanceState) { @@ -123,6 +123,14 @@ protected void createContent(final Bundle savedInstanceState) { "--force-device-scale-factor=1", }; CommandLine.getInstance().appendSwitchesAndArguments(cobaltCommandLineParams); + if (shouldSetJNIPrefix) { + CommandLine.getInstance().appendSwitchesAndArguments( + new String[] { + // Helps Kimono build avoid package name conflict with cronet. + "--cobalt-jni-prefix", + } + ); + } if (!VersionInfo.isOfficialBuild()) { String[] debugCommandLineParams = @@ -233,8 +241,6 @@ private void finishInitialization(Bundle savedInstanceState) { // Load the `url` with the same shell we created above. Log.i(TAG, "shellManager load url:" + shellUrl); mShellManager.getActiveShell().loadUrl(shellUrl); - - toggleFullscreenMode(true); } // Initially copied from ContentShellActiviy.java @@ -313,10 +319,6 @@ protected void shellHandleIntent(Intent intent) { } } - protected void toggleFullscreenMode(boolean enterFullscreen) { - LinearLayout toolBar = (LinearLayout) findViewById(R.id.toolbar); - toolBar.setVisibility(enterFullscreen ? View.GONE : View.VISIBLE); - } // Initially copied from ContentShellActiviy.java @Override diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index 7d7336e095de..cb3f5ec048f9 100644 --- a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -271,6 +271,9 @@ public void requestSuspend() { } } + // TODO(cobalt): remove when Kimono fully switches to Chrobalt. + public void requestStop(int errorLevel) {} + public boolean onSearchRequested() { // TODO(cobalt): re-enable native search request if needed. // if (applicationReady) { diff --git a/cobalt/android/apk/app/src/main/res/layout/content_shell_activity.xml b/cobalt/android/apk/app/src/main/res/layout/content_shell_activity.xml new file mode 100644 index 000000000000..45937868fc61 --- /dev/null +++ b/cobalt/android/apk/app/src/main/res/layout/content_shell_activity.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/cobalt/android/apk/app/src/main/res/values/strings.xml b/cobalt/android/apk/app/src/main/res/values/strings.xml index 8b7369fbca0a..4f6a11039443 100644 --- a/cobalt/android/apk/app/src/main/res/values/strings.xml +++ b/cobalt/android/apk/app/src/main/res/values/strings.xml @@ -1,33 +1,26 @@ - - - Can\'t connect right now - - Try again - - Open network settings - - Account not available. - - Account authorization failed. - - Account not available:\n%1$s - - Sign in requires Contacts permission to be granted in system settings. + Type URL Here + Initialization failed. + + Can\'t connect right now + + Try again + + Open network settings + + Account not available. + + Account authorization failed. + + Account not available:\n%1$s + + Sign in requires Contacts permission to be granted in system settings. diff --git a/cobalt/android/apk/app/src/org/chromium/content_shell/Shell.java b/cobalt/android/apk/app/src/org/chromium/content_shell/Shell.java new file mode 100644 index 000000000000..218467212e0f --- /dev/null +++ b/cobalt/android/apk/app/src/org/chromium/content_shell/Shell.java @@ -0,0 +1,292 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content_shell; + +import android.content.Context; +import android.graphics.Rect; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import org.chromium.base.Callback; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.components.embedder_support.view.ContentView; +import org.chromium.components.embedder_support.view.ContentViewRenderView; +import org.chromium.content_public.browser.ActionModeCallbackHelper; +import org.chromium.content_public.browser.LoadUrlParams; +import org.chromium.content_public.browser.NavigationController; +import org.chromium.content_public.browser.SelectionPopupController; +import org.chromium.content_public.browser.WebContents; +import org.chromium.ui.base.ViewAndroidDelegate; +import org.chromium.ui.base.WindowAndroid; + +// Cobalt's own Java implementation of Shell. It does not have Shell UI. + +/** + * Container for the various UI components that make up a shell window. + */ +@JNINamespace("content") +public class Shell extends LinearLayout { + + private static final long COMPLETED_PROGRESS_TIMEOUT_MS = 200; + + // Stylus handwriting: Setting this ime option instructs stylus writing service to restrict + // capturing writing events slightly outside the Url bar area. This is needed to prevent stylus + // handwriting in inputs in web content area that are very close to url bar area, from being + // committed to Url bar's Edit text. Ex: google.com search field. + private static final String IME_OPTION_RESTRICT_STYLUS_WRITING_AREA = + "restrictDirectWritingArea=true"; + + private WebContents mWebContents; + private NavigationController mNavigationController; + + private long mNativeShell; + private ContentViewRenderView mContentViewRenderView; + private WindowAndroid mWindow; + private ShellViewAndroidDelegate mViewAndroidDelegate; + + private boolean mLoading; + private boolean mIsFullscreen; + + private Callback mOverlayModeChangedCallbackForTesting; + + /** + * Constructor for inflating via XML. + */ + public Shell(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Set the SurfaceView being rendered to as soon as it is available. + */ + public void setContentViewRenderView(ContentViewRenderView contentViewRenderView) { + FrameLayout contentViewHolder = (FrameLayout) findViewById(R.id.contentview_holder); + if (contentViewRenderView == null) { + if (mContentViewRenderView != null) { + contentViewHolder.removeView(mContentViewRenderView); + } + } else { + contentViewHolder.addView(contentViewRenderView, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + } + mContentViewRenderView = contentViewRenderView; + } + + /** + * Initializes the Shell for use. + * + * @param nativeShell The pointer to the native Shell object. + * @param window The owning window for this shell. + */ + public void initialize(long nativeShell, WindowAndroid window) { + mNativeShell = nativeShell; + mWindow = window; + } + + /** + * Closes the shell and cleans up the native instance, which will handle destroying all + * dependencies. + */ + public void close() { + if (mNativeShell == 0) return; + ShellJni.get().closeShell(mNativeShell); + } + + @CalledByNative + private void onNativeDestroyed() { + mWindow = null; + mNativeShell = 0; + mWebContents = null; + } + + /** + * @return Whether the Shell has been destroyed. + * @see #onNativeDestroyed() + */ + public boolean isDestroyed() { + return mNativeShell == 0; + } + + /** + * @return Whether or not the Shell is loading content. + */ + public boolean isLoading() { + return mLoading; + } + + /** + * Loads an URL. This will perform minimal amounts of sanitizing of the URL to attempt to + * make it valid. + * + * @param url The URL to be loaded by the shell. + */ + public void loadUrl(String url) { + if (url == null) return; + + if (TextUtils.equals(url, mWebContents.getLastCommittedUrl().getSpec())) { + mNavigationController.reload(true); + } else { + mNavigationController.loadUrl(new LoadUrlParams(sanitizeUrl(url))); + } + // TODO(aurimas): Remove this when crbug.com/174541 is fixed. + getContentView().clearFocus(); + getContentView().requestFocus(); + } + + /** + * Given an URL, this performs minimal sanitizing to ensure it will be valid. + * @param url The url to be sanitized. + * @return The sanitized URL. + */ + public static String sanitizeUrl(String url) { + if (url == null) return null; + if (url.startsWith("www.") || url.indexOf(":") == -1) url = "http://" + url; + return url; + } + + @SuppressWarnings("unused") + @CalledByNative + private void onUpdateUrl(String url) {} + + @SuppressWarnings("unused") + @CalledByNative + private void onLoadProgressChanged(double progress) {} + + @CalledByNative + private void toggleFullscreenModeForTab(boolean enterFullscreen) { + } + + @CalledByNative + private boolean isFullscreenForTabOrPending() { + return mIsFullscreen; + } + + @SuppressWarnings("unused") + @CalledByNative + private void setIsLoading(boolean loading) { + mLoading = loading; + } + + public ShellViewAndroidDelegate getViewAndroidDelegate() { + return mViewAndroidDelegate; + } + + /** + * Initializes the ContentView based on the native tab contents pointer passed in. + * @param webContents A {@link WebContents} object. + */ + @SuppressWarnings("unused") + @CalledByNative + private void initFromNativeTabContents(WebContents webContents) { + Context context = getContext(); + ContentView cv = + ContentView.createContentView(context, null /* eventOffsetHandler */, webContents); + mViewAndroidDelegate = new ShellViewAndroidDelegate(cv); + assert (mWebContents != webContents); + if (mWebContents != null) mWebContents.clearNativeReference(); + webContents.initialize( + "", mViewAndroidDelegate, cv, mWindow, WebContents.createDefaultInternalsHolder()); + mWebContents = webContents; + SelectionPopupController.fromWebContents(webContents) + .setActionModeCallback(defaultActionCallback()); + mNavigationController = mWebContents.getNavigationController(); + if (getParent() != null) mWebContents.onShow(); + ((FrameLayout) findViewById(R.id.contentview_holder)).addView(cv, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + cv.requestFocus(); + mContentViewRenderView.setCurrentWebContents(mWebContents); + } + + /** + * {link @ActionMode.Callback} that uses the default implementation in + * {@link SelectionPopupController}. + */ + private ActionMode.Callback2 defaultActionCallback() { + final ActionModeCallbackHelper helper = + SelectionPopupController.fromWebContents(mWebContents) + .getActionModeCallbackHelper(); + + return new ActionMode.Callback2() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + helper.onCreateActionMode(mode, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return helper.onPrepareActionMode(mode, menu); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return helper.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + helper.onDestroyActionMode(); + } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + helper.onGetContentRect(mode, view, outRect); + } + }; + } + + @CalledByNative + public void setOverlayMode(boolean useOverlayMode) { + mContentViewRenderView.setOverlayVideoMode(useOverlayMode); + if (mOverlayModeChangedCallbackForTesting != null) { + mOverlayModeChangedCallbackForTesting.onResult(useOverlayMode); + } + } + + public void setOverayModeChangedCallbackForTesting(Callback callback) { + mOverlayModeChangedCallbackForTesting = callback; + } + + /** + * Enable/Disable navigation(Prev/Next) button if navigation is allowed/disallowed + * in respective direction. + * @param controlId Id of button to update + * @param enabled enable/disable value + */ + @CalledByNative + private void enableUiControl(int controlId, boolean enabled) {} + + /** + * @return The {@link View} currently shown by this Shell. + */ + public View getContentView() { + ViewAndroidDelegate viewDelegate = mWebContents.getViewAndroidDelegate(); + return viewDelegate != null ? viewDelegate.getContainerView() : null; + } + + /** + * @return The {@link WebContents} currently managing the content shown by this Shell. + */ + public WebContents getWebContents() { + return mWebContents; + } + + @NativeMethods + interface Natives { + void closeShell(long shellPtr); + } +} diff --git a/cobalt/android/apk/app/src/org/chromium/content_shell/ShellManager.java b/cobalt/android/apk/app/src/org/chromium/content_shell/ShellManager.java new file mode 100644 index 000000000000..17b1e7ba22a8 --- /dev/null +++ b/cobalt/android/apk/app/src/org/chromium/content_shell/ShellManager.java @@ -0,0 +1,158 @@ +// Copyright 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.content_shell; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import org.chromium.base.ThreadUtils; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.NativeMethods; +import org.chromium.components.embedder_support.view.ContentViewRenderView; +import org.chromium.content_public.browser.WebContents; +import org.chromium.ui.base.WindowAndroid; + +/** + * Container and generator of ShellViews. + */ +@JNINamespace("content") +public class ShellManager extends FrameLayout { + + public static final String DEFAULT_SHELL_URL = "http://www.google.com"; + private WindowAndroid mWindow; + private Shell mActiveShell; + + private String mStartupUrl = DEFAULT_SHELL_URL; + + // The target for all content rendering. + private ContentViewRenderView mContentViewRenderView; + + /** + * Constructor for inflating via XML. + */ + public ShellManager(final Context context, AttributeSet attrs) { + super(context, attrs); + ShellManagerJni.get().init(this); + } + + /** + * @param window The window used to generate all shells. + */ + public void setWindow(WindowAndroid window) { + assert window != null; + mWindow = window; + mContentViewRenderView = new ContentViewRenderView(getContext()); + mContentViewRenderView.onNativeLibraryLoaded(window); + } + + /** + * @return The window used to generate all shells. + */ + public WindowAndroid getWindow() { + return mWindow; + } + + /** + * Get the ContentViewRenderView. + */ + public ContentViewRenderView getContentViewRenderView() { + return mContentViewRenderView; + } + + /** + * Sets the startup URL for new shell windows. + */ + public void setStartupUrl(String url) { + mStartupUrl = url; + } + + /** + * @return The currently visible shell view or null if one is not showing. + */ + public Shell getActiveShell() { + return mActiveShell; + } + + /** + * Creates a new shell pointing to the specified URL. + * @param url The URL the shell should load upon creation. + */ + public void launchShell(String url) { + ThreadUtils.assertOnUiThread(); + Shell previousShell = mActiveShell; + ShellManagerJni.get().launchShell(url); + if (previousShell != null) previousShell.close(); + } + + @SuppressWarnings("unused") + @CalledByNative + private Object createShell(long nativeShellPtr) { + if (mContentViewRenderView == null) { + mContentViewRenderView = new ContentViewRenderView(getContext()); + mContentViewRenderView.onNativeLibraryLoaded(mWindow); + } + Shell shellView = new Shell(getContext(), null); + shellView.setId(R.id.container); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT); + FrameLayout frameLayout = new FrameLayout(getContext()); + frameLayout.setId(R.id.contentview_holder); + shellView.addView(frameLayout); + + shellView.initialize(nativeShellPtr, mWindow); + + // TODO(tedchoc): Allow switching back to these inactive shells. + if (mActiveShell != null) removeShell(mActiveShell); + + showShell(shellView); + return shellView; + } + + private void showShell(Shell shellView) { + shellView.setContentViewRenderView(mContentViewRenderView); + addView(shellView, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + mActiveShell = shellView; + WebContents webContents = mActiveShell.getWebContents(); + if (webContents != null) { + mContentViewRenderView.setCurrentWebContents(webContents); + webContents.onShow(); + } + } + + @CalledByNative + private void removeShell(Shell shellView) { + if (shellView == mActiveShell) mActiveShell = null; + if (shellView.getParent() == null) return; + shellView.setContentViewRenderView(null); + removeView(shellView); + } + + /** + * Destroys the Shell manager and associated components. + * Always called at activity exit, and potentially called by native in cases where we need to + * control the timing of mContentViewRenderView destruction. Must handle being called twice. + */ + @CalledByNative + public void destroy() { + // Remove active shell (Currently single shell support only available). + if (mActiveShell != null) { + removeShell(mActiveShell); + } + if (mContentViewRenderView != null) { + mContentViewRenderView.destroy(); + mContentViewRenderView = null; + } + } + + @NativeMethods + interface Natives { + void init(Object shellManagerInstance); + void launchShell(String url); + } +} diff --git a/cobalt/android/apk/app/src/org/chromium/content_shell/ShellViewAndroidDelegate.java b/cobalt/android/apk/app/src/org/chromium/content_shell/ShellViewAndroidDelegate.java new file mode 100644 index 000000000000..caa578cfef5d --- /dev/null +++ b/cobalt/android/apk/app/src/org/chromium/content_shell/ShellViewAndroidDelegate.java @@ -0,0 +1,59 @@ +// 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. + +package org.chromium.content_shell; + +import android.graphics.Bitmap; +import android.view.ViewGroup; + +import org.chromium.ui.base.ViewAndroidDelegate; +import org.chromium.ui.mojom.CursorType; + +/** + * Implementation of the abstract class {@link ViewAndroidDelegate} for content shell. + * Extended for testing. + */ +public class ShellViewAndroidDelegate extends ViewAndroidDelegate { + /** + * An interface delegates a {@link CallbackHelper} for cursor update. see more in {@link + * ContentViewPointerTypeTest.OnCursorUpdateHelperImpl}. + */ + public interface OnCursorUpdateHelper { + /** + * Record the last notifyCalled pointer type, see more {@link CallbackHelper#notifyCalled}. + * @param type The pointer type of the notifyCalled. + */ + void notifyCalled(int type); + } + + private OnCursorUpdateHelper mOnCursorUpdateHelper; + + public ShellViewAndroidDelegate(ViewGroup containerView) { + super(containerView); + } + + public void setOnCursorUpdateHelper(OnCursorUpdateHelper helper) { + mOnCursorUpdateHelper = helper; + } + + public OnCursorUpdateHelper getOnCursorUpdateHelper() { + return mOnCursorUpdateHelper; + } + + @Override + public void onCursorChangedToCustom(Bitmap customCursorBitmap, int hotspotX, int hotspotY) { + super.onCursorChangedToCustom(customCursorBitmap, hotspotX, hotspotY); + if (mOnCursorUpdateHelper != null) { + mOnCursorUpdateHelper.notifyCalled(CursorType.CUSTOM); + } + } + + @Override + public void onCursorChanged(int cursorType) { + super.onCursorChanged(cursorType); + if (mOnCursorUpdateHelper != null) { + mOnCursorUpdateHelper.notifyCalled(cursorType); + } + } +} diff --git a/cobalt/build/configs/android_common.gn b/cobalt/build/configs/android_common.gn index 1507836bd621..70c480704802 100644 --- a/cobalt/build/configs/android_common.gn +++ b/cobalt/build/configs/android_common.gn @@ -4,6 +4,10 @@ import("//cobalt/build/configs/common.gn") # TODO(b/380339614): Remove this flag after resolving build errors. treat_warnings_as_errors = false +# hashed jni names make build artifact opaque for Kimono build. +use_hashed_jni_names = false +use_errorprone_java_compiler = false + # Overriding flag from //media/media_options.gni. # Cobalt doesn't use //third_party/ffmpeg. media_use_ffmpeg = false diff --git a/content/shell/BUILD.gn b/content/shell/BUILD.gn index 0c169741a534..7ae3160a2493 100644 --- a/content/shell/BUILD.gn +++ b/content/shell/BUILD.gn @@ -289,6 +289,10 @@ static_library("content_shell_lib") { "//components/variations", "//components/variations/service", "//components/web_cache/renderer", + + "//components/js_injection/browser:browser", + "//components/js_injection/renderer:renderer", + "//content:content_resources", "//content:dev_ui_content_resources", "//content/common:main_frame_counter", diff --git a/content/shell/browser/shell.cc b/content/shell/browser/shell.cc index 40835b205377..7e7cca4f7433 100644 --- a/content/shell/browser/shell.cc +++ b/content/shell/browser/shell.cc @@ -766,4 +766,104 @@ void Shell::TitleWasSet(NavigationEntry* entry) { g_platform->SetTitle(this, entry->GetTitle()); } +void Shell::PrimaryMainDocumentElementAvailable() { + LOG(WARNING) << "Primary doc element created"; + + // Quick hack to demo injecting scripts + // Create browser-side mojo service component + js_communication_host_ = + std::make_unique(web_contents_.get()); + + // Inject a script at document start for all origins + // const std::u16string script(u"console.log('Hello from JS injection');"); + +const std::u16string script = uR"( +function arrayBufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); // Encode binary string to Base64 +} + +function base64ToArrayBuffer(base64) { + const binaryString = window.atob(base64); // Decode Base64 string to binary string + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +class PlatformServiceClient { + constructor(name) { + this.name = name; + } + + send(data) { + // convert the ArrayBuffer to base64 string because java bridge only takes primitive types as input. + const convertToB64 = arrayBufferToBase64(data); + const responseData = Android_H5vccPlatformService.platformServiceSend(this.name, convertToB64); + if (responseData === "") { + return null; + } + + // response_data has the synchronize response data converted to base64 string. + // convert it to ArrayBuffer, and return the ArrayBuffer to client. + return base64ToArrayBuffer(responseData); + } + + close() { + Android_H5vccPlatformService.closePlatformService(this.name); + } +} + +function initializeH5vccPlatformService() { + console.log('initializeH5vccPlatformService'); + if (typeof Android_H5vccPlatformService === 'undefined') { + return; + } + + // On Chrobalt, register window.H5vccPlatformService + window.H5vccPlatformService = { + // Holds the callback functions for the platform services when open() is called. + callbacks: { + }, + callbackFromAndroid: (serviceID, dataFromJava) => { + const arrayBuffer = base64ToArrayBuffer(dataFromJava); + window.H5vccPlatformService.callbacks[serviceID].callback(serviceID, arrayBuffer); + }, + has: (name) => { + return Android_H5vccPlatformService.hasPlatformService(name); + }, + open: function(name, callback) { + if (typeof callback !== 'function') { + throw new Error("window.H5vccPlatformService.open(), missing or invalid callback function."); + } + + const serviceID = Object.keys(this.callbacks).length + 1; + // Store the callback with the service ID, name, and callback + window.H5vccPlatformService.callbacks[serviceID] = { + name: name, + callback: callback + }; + Android_H5vccPlatformService.openPlatformService(serviceID, name); + return new PlatformServiceClient(name); + }, + } +} + +console.log('Hello 1'); +initializeH5vccPlatformService(); +console.log('Hello 2'); +)"; + + + const std::vector allowed_origins({"*"}); + auto result = js_communication_host_->AddDocumentStartJavaScript(script, allowed_origins); + CHECK(!result.error_message); + // End inject +} + } // namespace content diff --git a/content/shell/browser/shell.h b/content/shell/browser/shell.h index bbe8635423e8..8403d75258c7 100644 --- a/content/shell/browser/shell.h +++ b/content/shell/browser/shell.h @@ -23,6 +23,8 @@ #include "ui/gfx/geometry/size.h" #include "ui/gfx/native_widget_types.h" +#include "components/js_injection/browser/js_communication_host.h" + class GURL; namespace content { @@ -226,6 +228,8 @@ class Shell : public WebContentsDelegate, public WebContentsObserver { void PrimaryPageChanged(Page& page) override; #endif + void PrimaryMainDocumentElementAvailable() override; + std::unique_ptr dialog_manager_; std::unique_ptr web_contents_; @@ -243,6 +247,8 @@ class Shell : public WebContentsDelegate, public WebContentsObserver { static std::vector windows_; static base::OnceCallback shell_created_callback_; + + std::unique_ptr js_communication_host_; }; } // namespace content diff --git a/content/shell/renderer/shell_content_renderer_client.cc b/content/shell/renderer/shell_content_renderer_client.cc index 6b60a6254a3e..f376aff8b243 100644 --- a/content/shell/renderer/shell_content_renderer_client.cc +++ b/content/shell/renderer/shell_content_renderer_client.cc @@ -45,6 +45,8 @@ #include "components/cdm/renderer/widevine_key_system_info.h" #endif // BUILDFLAG(USE_STARBOARD_MEDIA) +#include "components/js_injection/renderer/js_communication.h" + #if BUILDFLAG(ENABLE_PLUGINS) #include "ppapi/shared_impl/ppapi_switches.h" // nogncheck #endif @@ -222,6 +224,7 @@ void ShellContentRendererClient::RenderFrameCreated(RenderFrame* render_frame) { // browser tests. If we only create that for browser tests then the override // of this method in WebTestContentRendererClient would not be needed. new ShellRenderFrameObserver(render_frame); + new js_injection::JsCommunication(render_frame); } void ShellContentRendererClient::PrepareErrorPage( @@ -414,4 +417,11 @@ ShellContentRendererClient::CreatePrescientNetworking( render_frame); } +void ShellContentRendererClient::RunScriptsAtDocumentStart(RenderFrame* render_frame) { + LOG(WARNING) << "Should run scripts here"; + js_injection::JsCommunication* communication = + js_injection::JsCommunication::Get(render_frame); + communication->RunScriptsAtDocumentStart(); +} + } // namespace content diff --git a/content/shell/renderer/shell_content_renderer_client.h b/content/shell/renderer/shell_content_renderer_client.h index 34b17d5ad682..96bb3f506004 100644 --- a/content/shell/renderer/shell_content_renderer_client.h +++ b/content/shell/renderer/shell_content_renderer_client.h @@ -32,6 +32,9 @@ class ShellContentRendererClient : public ContentRendererClient { ShellContentRendererClient(); ~ShellContentRendererClient() override; + // JS Injection hook + void RunScriptsAtDocumentStart(RenderFrame* render_frame) override; + // ContentRendererClient implementation. void RenderThreadStarted() override; void ExposeInterfacesToBrowser(mojo::BinderMap* binders) override;