(input.obj()));
+}
+} // namespace jni_zero
+
+#endif // TEST_ANDROID_NATIVE_TEST_UTIL_H_
diff --git a/test/android/org/webrtc/native_test/NativeTestWebrtc.java b/test/android/org/webrtc/native_test/NativeTestWebrtc.java
new file mode 100644
index 0000000000..352f9c8b8e
--- /dev/null
+++ b/test/android/org/webrtc/native_test/NativeTestWebrtc.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+// Based on Chromium's https://source.chromium.org/chromium/chromium/src/+/main:testing/android/native_test/java/src/org/chromium/native_test/NativeTest.java
+// and Angle's https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/tests/test_utils/runner/android/java/src/com/android/angle/test/AngleNativeTest.java
+
+package org.webrtc.native_test;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Process;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import org.jni_zero.JNINamespace;
+import org.jni_zero.JniType;
+import org.jni_zero.NativeMethods;
+
+import org.webrtc.native_test.StrictModeContext;
+import org.chromium.build.gtest_apk.NativeTestIntent;
+import org.chromium.test.reporter.TestStatusReporter;
+
+import java.io.File;
+
+/** Helper to run tests inside Activity or NativeActivity. */
+@JNINamespace("webrtc::test::android")
+public class NativeTestWebrtc {
+ private static final String TAG = "NativeTestWebrtc";
+
+ private String mCommandLineFilePath;
+ private StringBuilder mCommandLineFlags = new StringBuilder();
+ private TestStatusReporter mReporter;
+ private boolean mRunInSubThread;
+ private String mStdoutFilePath;
+
+ private static class ReportingUncaughtExceptionHandler
+ implements Thread.UncaughtExceptionHandler {
+
+ private TestStatusReporter mReporter;
+ private Thread.UncaughtExceptionHandler mWrappedHandler;
+
+ public ReportingUncaughtExceptionHandler(
+ TestStatusReporter reporter, Thread.UncaughtExceptionHandler wrappedHandler) {
+ mReporter = reporter;
+ mWrappedHandler = wrappedHandler;
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ mReporter.uncaughtException(Process.myPid(), ex);
+ if (mWrappedHandler != null) mWrappedHandler.uncaughtException(thread, ex);
+ }
+ }
+
+ public void preCreate(Activity activity) {
+ String coverageDeviceFile =
+ activity.getIntent().getStringExtra(NativeTestIntent.EXTRA_COVERAGE_DEVICE_FILE);
+ if (coverageDeviceFile != null) {
+ try {
+ Os.setenv("LLVM_PROFILE_FILE", coverageDeviceFile, true);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "failed to set LLVM_PROFILE_FILE" + e.toString());
+ }
+ }
+ // Set TMPDIR to make perfetto_unittests not to use /data/local/tmp as a tmp directory.
+ try {
+ Os.setenv("TMPDIR", activity.getApplicationContext().getCacheDir().getPath(), false);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "failed to set TMPDIR" + e.toString());
+ }
+ }
+
+ public void postCreate(Activity activity) {
+ parseArgumentsFromIntent(activity, activity.getIntent());
+ mReporter = new TestStatusReporter(activity);
+ mReporter.testRunStarted(Process.myPid());
+ Thread.setDefaultUncaughtExceptionHandler(
+ new ReportingUncaughtExceptionHandler(
+ mReporter, Thread.getDefaultUncaughtExceptionHandler()));
+ }
+
+ private void parseArgumentsFromIntent(Activity activity, Intent intent) {
+ Log.i(TAG, "Extras:");
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ for (String s : extras.keySet()) {
+ Log.i(TAG, " " + s);
+ }
+ }
+
+ mCommandLineFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FILE);
+ if (mCommandLineFilePath == null) {
+ mCommandLineFilePath = "";
+ } else {
+ File commandLineFile = new File(mCommandLineFilePath);
+ if (!commandLineFile.isAbsolute()) {
+ mCommandLineFilePath =
+ Environment.getExternalStorageDirectory() + "/" + mCommandLineFilePath;
+ }
+ Log.i(TAG, "command line file path: " + mCommandLineFilePath);
+ }
+
+ String commandLineFlags = intent.getStringExtra(NativeTestIntent.EXTRA_COMMAND_LINE_FLAGS);
+ if (commandLineFlags != null) mCommandLineFlags.append(commandLineFlags);
+
+ mRunInSubThread = intent.hasExtra(NativeTestIntent.EXTRA_RUN_IN_SUB_THREAD);
+
+ String gtestFilter = intent.getStringExtra(NativeTestIntent.EXTRA_GTEST_FILTER);
+ if (gtestFilter != null) {
+ appendCommandLineFlags("--gtest_filter=" + gtestFilter);
+ }
+
+ mStdoutFilePath = intent.getStringExtra(NativeTestIntent.EXTRA_STDOUT_FILE);
+ }
+
+ public void appendCommandLineFlags(String flags) {
+ mCommandLineFlags.append(" ").append(flags);
+ }
+
+ public void postStart(final Activity activity, boolean forceRunInSubThread) {
+ final Runnable runTestsTask =
+ new Runnable() {
+ @Override
+ public void run() {
+ runTests(activity);
+ }
+ };
+
+ if (mRunInSubThread || forceRunInSubThread) {
+ // Post a task that posts a task that creates a new thread and runs tests on it.
+
+ // On L and M, the system posts a task to the main thread that prints to stdout
+ // from android::Layout (https://goo.gl/vZA38p). Chaining the subthread creation
+ // through multiple tasks executed on the main thread ensures that this task
+ // runs before we start running tests s.t. its output doesn't interfere with
+ // the test output. See crbug.com/678146 for additional context.
+
+ final Handler handler = new Handler();
+ final Runnable startTestThreadTask =
+ new Runnable() {
+ @Override
+ public void run() {
+ new Thread(runTestsTask).start();
+ }
+ };
+ final Runnable postTestStarterTask =
+ new Runnable() {
+ @Override
+ public void run() {
+ handler.post(startTestThreadTask);
+ }
+ };
+ handler.post(postTestStarterTask);
+ } else {
+ // Post a task to run the tests. This allows us to not block
+ // onCreate and still run tests on the main thread.
+ new Handler().post(runTestsTask);
+ }
+ }
+
+ private void runTests(Activity activity) {
+ Natives jni = NativeTestWebrtcJni.get();
+ String isolated_test_root = NativeTestWebrtc.getIsolatedTestRoot();
+ Log.i(TAG, "Calling into native code");
+ jni.runTests(
+ mCommandLineFlags.toString(),
+ mCommandLineFilePath,
+ mStdoutFilePath,
+ isolated_test_root);
+ Log.i(TAG, "Call into native code returned");
+ activity.finish();
+ mReporter.testRunFinished(Process.myPid());
+ }
+
+
+ // @CalledByNative
+ public static String getIsolatedTestRoot() {
+ try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
+ return Environment.getExternalStorageDirectory().getAbsolutePath()
+ + "/chromium_tests_root";
+ }
+ }
+
+ // Signal a failure of the native test loader to python scripts
+ // which run tests. For example, we look for
+ // RUNNER_FAILED build/android/test_package.py.
+ private void nativeTestFailed() {
+ Log.e(TAG, "[ RUNNER_FAILED ] could not load native library");
+ }
+
+ @NativeMethods
+ interface Natives {
+ void runTests(
+ @JniType("std::string") String commandLineFlags,
+ @JniType("std::string") String commandLineFilePath,
+ @JniType("std::string") String stdoutFilePath,
+ @JniType("std::string") String testDataDir);
+ }
+}
\ No newline at end of file
diff --git a/test/android/org/webrtc/native_test/RTCNativeTestApplication.java b/test/android/org/webrtc/native_test/RTCNativeTestApplication.java
new file mode 100644
index 0000000000..ce2cf4fbc0
--- /dev/null
+++ b/test/android/org/webrtc/native_test/RTCNativeTestApplication.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.native_test;
+
+import android.app.Application;
+import android.content.Context;
+
+/** Application class to be used by native_test apks. */
+public class RTCNativeTestApplication extends Application {
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ assert getBaseContext() != null;
+
+ // This is required for Mockito to initialize mocks without running under Instrumentation.
+ System.setProperty("org.mockito.android.target", getCacheDir().getPath());
+ }
+}
\ No newline at end of file
diff --git a/test/android/org/webrtc/native_test/RTCNativeUnitTest.java b/test/android/org/webrtc/native_test/RTCNativeUnitTest.java
index dede7edd1f..329e1e8b38 100644
--- a/test/android/org/webrtc/native_test/RTCNativeUnitTest.java
+++ b/test/android/org/webrtc/native_test/RTCNativeUnitTest.java
@@ -11,16 +11,39 @@
package org.webrtc.native_test;
import android.app.Activity;
-import org.chromium.native_test.NativeUnitTest;
+import android.util.Log;
+import org.chromium.build.NativeLibraries;
+import org.webrtc.native_test.NativeTestWebrtc;
import org.webrtc.ContextUtils;
/**
* Native unit test that calls ContextUtils.initialize for WebRTC.
*/
-public class RTCNativeUnitTest extends NativeUnitTest {
- @Override
- public void preCreate(Activity activity) {
- super.preCreate(activity);
- ContextUtils.initialize(activity.getApplicationContext());
- }
-}
+public class RTCNativeUnitTest extends NativeTestWebrtc {
+
+ private static final String TAG = "RTCNativeUnitTest";
+
+ private static final String LIBRARY_UNDER_TEST_NAME =
+ "org.chromium.native_test.NativeTestInstrumentationTestRunner.LibraryUnderTest";
+
+ @Override
+ public void preCreate(Activity activity) {
+ super.preCreate(activity);
+
+ // For NativeActivity based tests, dependency libraries must be loaded before
+ // NativeActivity::OnCreate, otherwise loading android.app.lib_name will fail
+ String libraryToLoad = activity.getIntent().getStringExtra(LIBRARY_UNDER_TEST_NAME);
+ loadLibraries(
+ libraryToLoad != null ? new String[] {libraryToLoad} : NativeLibraries.LIBRARIES);
+
+ ContextUtils.initialize(activity.getApplicationContext());
+ }
+
+ private void loadLibraries(String[] librariesToLoad) {
+ for (String library : librariesToLoad) {
+ Log.i(TAG, "loading: " + library);
+ System.loadLibrary(library);
+ Log.i(TAG, "loaded: " + library);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/android/org/webrtc/native_test/StrictModeContext.java b/test/android/org/webrtc/native_test/StrictModeContext.java
new file mode 100644
index 0000000000..24e036cb79
--- /dev/null
+++ b/test/android/org/webrtc/native_test/StrictModeContext.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+// Based on Chromium's https://source.chromium.org/chromium/chromium/src/+/main:base/android/java/src/org/chromium/base/StrictModeContext.java
+
+package org.webrtc.native_test;
+
+import android.os.Build;
+import android.os.StrictMode;
+
+import java.io.Closeable;
+
+/**
+ * Enables try-with-resources compatible StrictMode violation allowlisting.
+ *
+ * Prefer "ignored" as the variable name to appease Android Studio's "Unused symbol" inspection.
+ *
+ *
Example:
+ *
+ *
+ * try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
+ * return Example.doThingThatRequiresDiskWrites();
+ * }
+ *
+ */
+public class StrictModeContext implements Closeable {
+ private static class Impl extends StrictModeContext {
+ private final StrictMode.ThreadPolicy mThreadPolicy;
+ private final StrictMode.VmPolicy mVmPolicy;
+
+ private Impl(StrictMode.ThreadPolicy threadPolicy, StrictMode.VmPolicy vmPolicy) {
+ mThreadPolicy = threadPolicy;
+ mVmPolicy = vmPolicy;
+ }
+
+ private Impl(StrictMode.ThreadPolicy threadPolicy) {
+ this(threadPolicy, null);
+ }
+
+ private Impl(StrictMode.VmPolicy vmPolicy) {
+ this(null, vmPolicy);
+ }
+
+ @Override
+ public void close() {
+ if (mThreadPolicy != null) {
+ StrictMode.setThreadPolicy(mThreadPolicy);
+ }
+ if (mVmPolicy != null) {
+ StrictMode.setVmPolicy(mVmPolicy);
+ }
+ }
+ }
+
+ /**
+ * Convenience method for disabling all VM-level StrictMode checks with try-with-resources.
+ * Includes everything listed here:
+ * https://developer.android.com/reference/android/os/StrictMode.VmPolicy.Builder.html
+ */
+ public static StrictModeContext allowAllVmPolicies() {
+ StrictMode.VmPolicy oldPolicy = StrictMode.getVmPolicy();
+ StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX);
+ return new Impl(oldPolicy);
+ }
+
+ /**
+ * Convenience method for disabling all thread-level StrictMode checks with try-with-resources.
+ * Includes everything listed here:
+ * https://developer.android.com/reference/android/os/StrictMode.ThreadPolicy.Builder.html
+ */
+ public static StrictModeContext allowAllThreadPolicies() {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX);
+ return new Impl(oldPolicy);
+ }
+
+ /** Convenience method for disabling StrictMode for disk-writes with try-with-resources. */
+ public static StrictModeContext allowDiskWrites() {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ return new Impl(oldPolicy);
+ }
+
+ /** Convenience method for disabling StrictMode for disk-reads with try-with-resources. */
+ public static StrictModeContext allowDiskReads() {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+ return new Impl(oldPolicy);
+ }
+
+ /** Convenience method for disabling StrictMode for slow calls with try-with-resources. */
+ public static StrictModeContext allowSlowCalls() {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ StrictMode.setThreadPolicy(
+ new StrictMode.ThreadPolicy.Builder(oldPolicy).permitCustomSlowCalls().build());
+ return new Impl(oldPolicy);
+ }
+
+ /**
+ * Convenience method for disabling StrictMode for unbuffered input/output operations with
+ * try-with-resources. For API level 25- this method will do nothing; because
+ * StrictMode.ThreadPolicy.Builder#permitUnbufferedIo is added in API level 26.
+ */
+ public static StrictModeContext allowUnbufferedIo() {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ StrictMode.setThreadPolicy(
+ new StrictMode.ThreadPolicy.Builder(oldPolicy)
+ .permitUnbufferedIo()
+ .build());
+ }
+ return new Impl(oldPolicy);
+ }
+
+ @Override
+ public void close() {}
+}
\ No newline at end of file
diff --git a/webrtc.gni b/webrtc.gni
index 6a09451900..ba5c39da20 100644
--- a/webrtc.gni
+++ b/webrtc.gni
@@ -525,13 +525,17 @@ template("rtc_test") {
public_configs += invoker.public_configs
}
if (!build_with_chromium && is_android) {
+ use_default_launcher = false
android_manifest = webrtc_root + "test/android/AndroidManifest.xml"
use_raw_android_executable = false
min_sdk_version = 21
target_sdk_version = 23
deps += [
"//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
+ webrtc_root + "sdk/android:native_test_jni_onload",
+ webrtc_root + "sdk/android:base_java",
webrtc_root + "test:native_test_java",
+ webrtc_root + "test:native_test_support",
]
}