diff --git a/cobalt/android/BUILD.gn b/cobalt/android/BUILD.gn index 16515c1b20cc..eb439ee35f52 100644 --- a/cobalt/android/BUILD.gn +++ b/cobalt/android/BUILD.gn @@ -56,6 +56,9 @@ android_library("cobalt_apk_java") { "apk/app/src/main/java/dev/cobalt/coat/CaptionSettings.java", "apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java", "apk/app/src/main/java/dev/cobalt/coat/CobaltHttpHelper.java", + "apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObject.java", + "apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObjectExample.java", + "apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptInterface.java", # "apk/app/src/main/java/dev/cobalt/coat/CobaltMediaSession.java", "apk/app/src/main/java/dev/cobalt/coat/CobaltService.java", @@ -92,6 +95,7 @@ android_library("cobalt_apk_java") { # "apk/app/src/main/java/dev/cobalt/storage/CobaltStorageLoader.java", # "apk/app/src/main/java/dev/cobalt/storage/StorageProto.java", + "apk/app/src/main/java/dev/cobalt/util/AssetLoader.java", "apk/app/src/main/java/dev/cobalt/util/DisplayUtil.java", "apk/app/src/main/java/dev/cobalt/util/Holder.java", "apk/app/src/main/java/dev/cobalt/util/IsEmulator.java", @@ -105,6 +109,7 @@ android_library("cobalt_apk_java") { android_assets("cobalt_apk_assets") { testonly = true sources = [ + "apk/app/src/app/assets/example.js", "apk/app/src/app/assets/not_empty.txt", "apk/app/src/app/assets/test/not_empty.txt", "apk/app/src/app/assets/web/cobalt_blue_splash_screen.css", diff --git a/cobalt/android/apk/app/src/app/assets/example.js b/cobalt/android/apk/app/src/app/assets/example.js new file mode 100644 index 000000000000..0bdc7beb2481 --- /dev/null +++ b/cobalt/android/apk/app/src/app/assets/example.js @@ -0,0 +1 @@ +AndroidExample.testJavaScriptMethod(); 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 60467c157d9a..526768080734 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 @@ -23,13 +23,19 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.util.Pair; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.widget.FrameLayout; +import dev.cobalt.coat.javabridge.CobaltJavaScriptAndroidObject; +import dev.cobalt.coat.javabridge.CobaltJavaScriptAndroidObjectExample; +import dev.cobalt.coat.javabridge.CobaltJavaScriptInterface; import dev.cobalt.media.MediaCodecCapabilitiesLogger; import dev.cobalt.media.VideoSurfaceView; +import dev.cobalt.util.AssetLoader; import dev.cobalt.util.DisplayUtil; import dev.cobalt.util.Log; import dev.cobalt.util.UsedByNative; @@ -38,7 +44,10 @@ import java.util.List; import java.util.Locale; import java.util.regex.Pattern; +import org.chromium.content_public.browser.JavascriptInjector; +import org.chromium.content_public.browser.WebContents; import org.chromium.content_shell_apk.ContentShellActivity; + // import dev.cobalt.media.AudioOutputManager; /** Native activity that has the required JNI methods called by the Starboard implementation. */ @@ -66,6 +75,8 @@ public abstract class CobaltActivity extends ContentShellActivity { private static final Pattern URL_PARAM_PATTERN = Pattern.compile("^[a-zA-Z0-9_=]*$"); + public static final int JAVA_BRIDGE_INITIALIZATION_DELAY_MILLI_SECONDS = 100; + private VideoSurfaceView videoSurfaceView; private boolean forceCreateNewVideoSurfaceView = false; @@ -105,6 +116,55 @@ protected void onCreate(Bundle savedInstanceState) { videoSurfaceView = new VideoSurfaceView(this); addContentView( videoSurfaceView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + + initializeJavaBridge(); + } + + /** + * Initializes the Java Bridge to allow communication between Java and JavaScript. + * This method injects Java objects into the WebView and loads corresponding JavaScript code. + */ + private void initializeJavaBridge() { + + WebContents webContents = getActiveWebContents(); + if (webContents == null) { + // WebContents not initialized yet, post a delayed runnable to check again + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + initializeJavaBridge(); // Recursive call to check again + } + }, JAVA_BRIDGE_INITIALIZATION_DELAY_MILLI_SECONDS); + return; + } + + // --- Initialize the Java Bridge --- + + // 1. Gather all Java objects that need to be exposed to JavaScript. + List javaScriptAndroidObjectList = new ArrayList<>(); + javaScriptAndroidObjectList.add(new CobaltJavaScriptAndroidObjectExample()); + + // 2. Use JavascriptInjector to inject Java objects into the WebView. + // This makes the annotated methods in these objects accessible from JavaScript. + JavascriptInjector javascriptInjector = JavascriptInjector.fromWebContents(webContents, false); + if (javascriptInjector == null) { + Log.w(TAG, "javascriptInjector is null, failed to init Java Bridge."); + return; + } + + javascriptInjector.setAllowInspection(true); + for (CobaltJavaScriptAndroidObject javascriptAndroidObject : javaScriptAndroidObjectList) { + javascriptInjector.addPossiblyUnsafeInterface(javascriptAndroidObject, javascriptAndroidObject.getJavaScriptInterfaceName(), CobaltJavaScriptInterface.class); + } + + // 3. Load and evaluate JavaScript code that interacts with the injected Java objects. + for (CobaltJavaScriptAndroidObject javaScriptAndroidObject : javaScriptAndroidObjectList) { + String jsFileName = javaScriptAndroidObject.getJavaScriptAssetName(); + if (jsFileName != null) { + String jsCode = AssetLoader.loadJavaScriptFromAssets(this, jsFileName); + webContents.evaluateJavaScript(jsCode, null); + } + } } /** diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObject.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObject.java new file mode 100644 index 000000000000..a3a4b43121bc --- /dev/null +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObject.java @@ -0,0 +1,25 @@ +package dev.cobalt.coat.javabridge; + +import androidx.annotation.Nullable; + +/** + * Interface for Android objects that are exposed to JavaScript. + */ +public interface CobaltJavaScriptAndroidObject { + + /** + * Gets the name used to expose this object to JavaScript. + * This name is used in the `addJavascriptInterface` method of the WebView. + * + * @return The JavaScript interface name. + */ + public String getJavaScriptInterfaceName(); + + /** + * Gets the name of the JavaScript asset file that uses this interface. + * This allows the JavaScript code to be loaded and interact with this object. + * + * @return The name of the JavaScript asset file, or null if not applicable. + */ + public @Nullable String getJavaScriptAssetName(); +} diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObjectExample.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObjectExample.java new file mode 100644 index 000000000000..d909e7d8abe0 --- /dev/null +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptAndroidObjectExample.java @@ -0,0 +1,26 @@ +package dev.cobalt.coat.javabridge; + +import static dev.cobalt.util.Log.TAG; + +import android.util.Log; + +/** + * A simple example of implement CobaltJavaScriptAndroidObject. + */ +public class CobaltJavaScriptAndroidObjectExample implements CobaltJavaScriptAndroidObject { + + @Override + public String getJavaScriptInterfaceName() { + return "AndroidExample"; + } + + @Override + public String getJavaScriptAssetName() { + return "example.js"; + } + + @CobaltJavaScriptInterface + public void testJavaScriptMethod() { + Log.w(TAG, "Hello world"); + } +} diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptInterface.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptInterface.java new file mode 100644 index 000000000000..9a14da6d4c90 --- /dev/null +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/coat/javabridge/CobaltJavaScriptInterface.java @@ -0,0 +1,17 @@ +package dev.cobalt.coat.javabridge; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that allows exposing methods to JavaScript. Starting from API level + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} and above, methods explicitly + * marked with this annotation are available to the Javascript code. + */ +@SuppressWarnings("javadoc") +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface CobaltJavaScriptInterface { +} diff --git a/cobalt/android/apk/app/src/main/java/dev/cobalt/util/AssetLoader.java b/cobalt/android/apk/app/src/main/java/dev/cobalt/util/AssetLoader.java new file mode 100644 index 000000000000..06585e3992dd --- /dev/null +++ b/cobalt/android/apk/app/src/main/java/dev/cobalt/util/AssetLoader.java @@ -0,0 +1,29 @@ +package dev.cobalt.util; + +import static dev.cobalt.util.Log.TAG; + +import android.content.Context; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.io.IOException; + +/** Utility functions for read asset. */ +public class AssetLoader { + + private AssetLoader() {} + + public static String loadJavaScriptFromAssets(Context context, String filename) { + try { + InputStream is = context.getAssets().open(filename); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + return new String(buffer, StandardCharsets.UTF_8); + } catch (IOException ex) { + String error = "asset " + filename + " failed to load"; + Log.e(TAG, error); + return String.format("console.error('%s');", error); + } + } +} diff --git a/content/app/content_main_runner_impl.cc b/content/app/content_main_runner_impl.cc index 38c5f465d8e4..111d89d87625 100644 --- a/content/app/content_main_runner_impl.cc +++ b/content/app/content_main_runner_impl.cc @@ -675,6 +675,8 @@ int NO_STACK_PROTECTOR RunZygote(ContentMainDelegate* delegate) { return kMainFunctions[i].function(std::move(main_params)); } + content::RenderFrameHost::AllowInjectingJavaScript(); + auto exit_code = delegate->RunProcess(process_type, std::move(main_params)); DCHECK(absl::holds_alternative(exit_code)); DCHECK_GE(absl::get(exit_code), 0);