Create a custom test launcher for android

Set use_default_launcher=false in rtc_test on android

Bug: webrtc:42223878
Change-Id: If05da40b420d5da8f9e0f39560eb07380ebada14
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/368921
Owners-Override: Jeremy Leconte <jleconte@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Jeremy Leconte <jleconte@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Commit-Queue: Jeremy Leconte <jleconte@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#43505}
diff --git a/BUILD.gn b/BUILD.gn
index b3c84aa..d829c0f 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -695,7 +695,6 @@
       deps += [
         "sdk/android:native_unittests",
         "sdk/android:native_unittests_java",
-        "//testing/android/native_test:native_test_support",
       ]
       shard_timeout = 900
     }
@@ -744,11 +743,7 @@
     data = video_engine_tests_resources
     if (is_android) {
       use_default_launcher = false
-      deps += [
-        "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-        "//testing/android/native_test:native_test_java",
-        "//testing/android/native_test:native_test_support",
-      ]
+      deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
       shard_timeout = 900
     }
     if (is_ios) {
@@ -793,11 +788,7 @@
     data = webrtc_perf_tests_resources
     if (is_android) {
       use_default_launcher = false
-      deps += [
-        "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-        "//testing/android/native_test:native_test_java",
-        "//testing/android/native_test:native_test_support",
-      ]
+      deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
       shard_timeout = 4500
     }
     if (is_ios) {
@@ -809,7 +800,6 @@
     testonly = true
     deps = [ "rtc_base:rtc_base_nonparallel_tests" ]
     if (is_android) {
-      deps += [ "//testing/android/native_test:native_test_support" ]
       shard_timeout = 900
     }
   }
diff --git a/common_audio/BUILD.gn b/common_audio/BUILD.gn
index 1342eef..4845bc7 100644
--- a/common_audio/BUILD.gn
+++ b/common_audio/BUILD.gn
@@ -388,8 +388,6 @@
     ]
 
     if (is_android) {
-      deps += [ "//testing/android/native_test:native_test_support" ]
-
       shard_timeout = 900
     }
   }
diff --git a/common_video/BUILD.gn b/common_video/BUILD.gn
index 4cff570..4c4c8c9 100644
--- a/common_video/BUILD.gn
+++ b/common_video/BUILD.gn
@@ -219,7 +219,6 @@
 
     data = common_video_resources
     if (is_android) {
-      deps += [ "//testing/android/native_test:native_test_support" ]
       shard_timeout = 900
     }
 
diff --git a/media/BUILD.gn b/media/BUILD.gn
index 1b8f4cc..7a27f40 100644
--- a/media/BUILD.gn
+++ b/media/BUILD.gn
@@ -1074,7 +1074,6 @@
       data = rtc_media_unittests_resources
 
       if (is_android) {
-        deps += [ "//testing/android/native_test:native_test_support" ]
         shard_timeout = 900
       }
 
diff --git a/modules/BUILD.gn b/modules/BUILD.gn
index 52bd86f..76fcf08 100644
--- a/modules/BUILD.gn
+++ b/modules/BUILD.gn
@@ -83,8 +83,6 @@
         # rtc_test targets. Therefore we include this target here, instead of
         # in video_coding_modules_tests, where it is actually used.
         "../sdk/android:libjingle_peerconnection_java",
-        "//sdk/android:native_test_jni_onload",
-        "//testing/android/native_test:native_test_support",
       ]
       shard_timeout = 900
     }
@@ -238,10 +236,7 @@
 
     if (is_android) {
       use_default_launcher = false
-      deps += [
-        "../sdk/android:libjingle_peerconnection_java",
-        "//testing/android/native_test:native_test_support",
-      ]
+      deps += [ "../sdk/android:libjingle_peerconnection_java" ]
       shard_timeout = 900
     }
     if (is_ios) {
diff --git a/modules/audio_coding/BUILD.gn b/modules/audio_coding/BUILD.gn
index bdc4860..3511b8f 100644
--- a/modules/audio_coding/BUILD.gn
+++ b/modules/audio_coding/BUILD.gn
@@ -1003,11 +1003,7 @@
 
       if (is_android) {
         use_default_launcher = false
-        deps += [
-          "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-          "//testing/android/native_test:native_test_java",
-          "//testing/android/native_test:native_test_support",
-        ]
+        deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
         shard_timeout = 900
       }
       if (is_ios) {
@@ -1099,11 +1095,7 @@
 
       if (is_android) {
         use_default_launcher = false
-        deps += [
-          "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-          "//testing/android/native_test:native_test_java",
-          "//testing/android/native_test:native_test_support",
-        ]
+        deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
         shard_timeout = 900
       }
 
diff --git a/modules/audio_device/BUILD.gn b/modules/audio_device/BUILD.gn
index b69ada7..aa4d216 100644
--- a/modules/audio_device/BUILD.gn
+++ b/modules/audio_device/BUILD.gn
@@ -493,7 +493,6 @@
         "../../sdk/android:internal_jni",
         "../../sdk/android:libjingle_peerconnection_java",
         "../../sdk/android:native_api_jni",
-        "../../sdk/android:native_test_jni_onload",
         "../utility",
       ]
     }
diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn
index c5c6ce1..2318bcd 100644
--- a/modules/video_coding/BUILD.gn
+++ b/modules/video_coding/BUILD.gn
@@ -1000,8 +1000,6 @@
       deps += [
         ":android_codec_factory_helper",
         "../../sdk/android:hwcodecs_java",
-        "//sdk/android:native_test_jni_onload",
-        "//testing/android/native_test:native_test_support",
       ]
       shard_timeout = 900
     }
diff --git a/modules/video_coding/DEPS b/modules/video_coding/DEPS
index d62707c..49c640b 100644
--- a/modules/video_coding/DEPS
+++ b/modules/video_coding/DEPS
@@ -11,9 +11,6 @@
 ]
 
 specific_include_rules = {
-  "android_codec_factory_helper\.cc": [
-    "+base/android",
-  ],
   "multiplex_encoder_adapter\.cc": [
     "+media/base",
   ],
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index 7193349..c39cf40 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -2098,11 +2098,7 @@
 
     if (is_android) {
       use_default_launcher = false
-      deps += [
-        "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-        "//testing/android/native_test:native_test_java",
-        "//testing/android/native_test:native_test_support",
-      ]
+      deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
     }
   }
 
@@ -2448,8 +2444,7 @@
 
         # We need to depend on this one directly, or classloads will fail for
         # the voice engine BuildInfo, for instance.
-        "//sdk/android:libjingle_peerconnection_java",
-        "//sdk/android:native_test_jni_onload",
+        "../sdk/android:libjingle_peerconnection_java",
       ]
       shard_timeout = 900
     }
@@ -2519,12 +2514,11 @@
         "test/android_test_initializer.h",
       ]
       deps = [
+        "../modules/utility:utility",
+        "../rtc_base:checks",
         "../rtc_base:ssl_adapter",
         "../sdk/android:internal_jni",
         "../sdk/android:libjingle_peerconnection_jni",
-        "//modules/utility:utility",
-        "//rtc_base:checks",
-        "//testing/android/native_test:native_test_support",
       ]
     }
   }
@@ -2818,7 +2812,6 @@
     sources = [ "test/svc_e2e_tests.cc" ]
     data = svc_tests_resources
     deps = [
-      "..//test/network:simulated_network",
       "../api:create_network_emulation_manager",
       "../api:create_peer_connection_quality_test_frame_generator",
       "../api:create_peerconnection_quality_test_fixture",
@@ -2843,6 +2836,7 @@
       "../test:fileutils",
       "../test:test_main",
       "../test:test_support",
+      "../test/network:simulated_network",
       "../test/pc/e2e:network_quality_metrics_reporter",
       "../test/pc/e2e/analyzer/video:default_video_quality_analyzer",
     ]
diff --git a/pc/DEPS b/pc/DEPS
index 36439a3..227cf27 100644
--- a/pc/DEPS
+++ b/pc/DEPS
@@ -18,9 +18,6 @@
 ]
 
 specific_include_rules = {
-  "androidtestinitializer\.cc": [
-    "+base/android",  # Allowed only for Android tests.
-  ],
   "srtpfilter_unittest\.cc": [
     "+crypto",
   ],
diff --git a/rtc_tools/BUILD.gn b/rtc_tools/BUILD.gn
index 68521ba..8f8e355 100644
--- a/rtc_tools/BUILD.gn
+++ b/rtc_tools/BUILD.gn
@@ -614,7 +614,6 @@
 
       data = tools_unittests_resources
       if (is_android) {
-        deps += [ "//testing/android/native_test:native_test_support" ]
         shard_timeout = 900
       }
       if (is_ios) {
diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn
index a051da0..8295d2f 100644
--- a/sdk/android/BUILD.gn
+++ b/sdk/android/BUILD.gn
@@ -91,7 +91,7 @@
       ":surfaceviewrenderer_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -140,6 +140,7 @@
       ":native_api_jni",
       ":video_egl_jni",
       "../../pc:libjingle_peerconnection",
+      "../../rtc_base:logging",
       "../../rtc_base:ssl_adapter",
       "//third_party/jni_zero",
     ]
@@ -169,11 +170,13 @@
       "src/java/org/webrtc/WebRtcClassLoader.java",
     ]
 
+    srcjar_deps = [ "//sdk/android:generated_native_api_jni" ]
+
     deps = [
       ":generated_base_jni_java",
       ":generated_native_api_jni_java",
       ":generated_rtcerror_jni_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -188,7 +191,7 @@
 
     deps = [
       ":base_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -210,7 +213,7 @@
     deps = [
       ":base_java",
       ":generated_video_jni_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
     srcjar_deps = [ "//api/video:video_frame_enums" ]
@@ -258,7 +261,7 @@
       ":generated_video_egl_jni_java",
       ":generated_video_jni_java",
       ":video_api_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -335,7 +338,7 @@
       ":swcodecs_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
     srcjar_deps = [
@@ -366,7 +369,7 @@
       ":base_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -398,7 +401,7 @@
       ":base_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -426,7 +429,7 @@
       ":base_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -450,7 +453,7 @@
       ":generated_audio_device_module_base_jni_java",
       ":generated_java_audio_device_module_native_jni_java",
       ":generated_java_audio_jni_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -475,7 +478,7 @@
     deps = [
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -491,7 +494,7 @@
       ":base_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -506,7 +509,7 @@
       ":generated_libvpx_vp8_jni_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -521,7 +524,7 @@
       ":generated_libvpx_vp9_jni_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -533,7 +536,7 @@
       ":generated_libaom_av1_encoder_jni_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -561,7 +564,7 @@
       ":libvpx_vp9_java",
       ":video_api_java",
       ":video_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
       "//third_party/androidx:androidx_annotation_annotation_java",
     ]
   }
@@ -970,9 +973,9 @@
       ":generated_external_classes_jni",
       ":generated_native_api_jni",
       ":internal_jni",
+      "../../api:array_view",
       "../../api:sequence_checker",
-      "//api:array_view",
-      "//rtc_base:checks",
+      "../../rtc_base:checks",
       "//third_party/jni_zero",
     ]
   }
@@ -987,7 +990,7 @@
     deps = [
       ":base_jni",
       ":native_api_jni",
-      "//rtc_base:checks",
+      "../../rtc_base:checks",
     ]
   }
 
@@ -1035,8 +1038,8 @@
       ":base_jni",
       ":native_api_jni",
       ":video_jni",
-      "//api/video_codecs:video_codecs_api",
-      "//rtc_base:checks",
+      "../../api/video_codecs:video_codecs_api",
+      "../../rtc_base:checks",
       "//third_party/jni_zero",
     ]
   }
@@ -1067,9 +1070,9 @@
     deps = [
       ":base_jni",
       ":peerconnection_jni",
+      "../../api:libjingle_peerconnection_api",
+      "../../api/video_codecs:video_codecs_api",
       "../../rtc_base:threading",
-      "//api:libjingle_peerconnection_api",
-      "//api/video_codecs:video_codecs_api",
     ]
   }
 
@@ -1105,12 +1108,12 @@
       ":native_api_jni",
       ":video_jni",
       ":videoframe_jni",
+      "../../api:libjingle_peerconnection_api",
+      "../../api:media_stream_interface",
+      "../../api/video:video_frame",
+      "../../api/video:video_rtp_headers",
       "../../rtc_base:refcount",
       "../../rtc_base:threading",
-      "//api:libjingle_peerconnection_api",
-      "//api:media_stream_interface",
-      "//api/video:video_frame",
-      "//api/video:video_rtp_headers",
       "//third_party/jni_zero",
     ]
   }
@@ -1125,7 +1128,7 @@
     deps = [
       ":base_java",
       ":generated_logging_jni_java",
-      "//rtc_base:base_java",
+      "../../rtc_base:base_java",
     ]
   }
 
@@ -1571,7 +1574,7 @@
         ":swcodecs_java",
         ":video_api_java",
         ":video_java",
-        "//rtc_base:base_java",
+        "../../rtc_base:base_java",
         "//third_party/android_deps:guava_android_java",
         "//third_party/androidx:androidx_annotation_annotation_java",
         "//third_party/androidx:androidx_test_monitor_java",
@@ -1603,6 +1606,7 @@
       ":libjingle_peerconnection_metrics_default_jni",
       ":native_api_jni",
       "../../pc:libjingle_peerconnection",
+      "../../rtc_base:logging",
       "../../rtc_base:ssl_adapter",
     ]
     output_extension = "so"
@@ -1631,11 +1635,13 @@
     sources = [ "native_unittests/test_jni_onload.cc" ]
 
     deps = [
+      ":base_java",
       ":base_jni",
       ":internal_jni",
       ":native_api_base",
       ":native_api_jni",
       "../../rtc_base:checks",
+      "../../rtc_base:logging",
     ]
   }
 
@@ -1668,7 +1674,6 @@
       ":native_api_peerconnection",
       ":native_api_stacktrace",
       ":native_api_video",
-      ":native_test_jni_onload",
       ":opensles_audio_device_module",
       ":video_jni",
       "../../api:enable_media_with_defaults",
diff --git a/sdk/android/native_unittests/test_jni_onload.cc b/sdk/android/native_unittests/test_jni_onload.cc
index dafe49c..91b2f2e 100644
--- a/sdk/android/native_unittests/test_jni_onload.cc
+++ b/sdk/android/native_unittests/test_jni_onload.cc
@@ -13,11 +13,21 @@
 #define JNIEXPORT __attribute__((visibility("default")))
 
 #include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
 #include "sdk/android/native_api/base/init.h"
 #include "sdk/android/native_api/jni/java_types.h"
+#include "test/android/native_test_launcher.h"  // nogncheck
 
 // This is called by the VM when the shared library is first loaded.
 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+  RTC_LOG(LS_INFO) << "Entering JNI_OnLoad in test_jni_onload.cc";
+
+  jni_zero::InitVM(vm);
+  // TODO(webrtc:42223878): Set exception handler?
+  // jni_zero::SetExceptionHandler(CheckException);
+  // JNIEnv* env = jni_zero::AttachCurrentThread();
+  // TODO(webrtc:42223878): Classloader, OOM error handler?
   webrtc::InitAndroid(vm);
+  webrtc::test::android::InstallHandlers();
   return JNI_VERSION_1_4;
 }
diff --git a/sdk/android/src/jni/audio_device/DEPS b/sdk/android/src/jni/audio_device/DEPS
index 9a3adee..7ee1155 100644
--- a/sdk/android/src/jni/audio_device/DEPS
+++ b/sdk/android/src/jni/audio_device/DEPS
@@ -1,4 +1,3 @@
 include_rules = [
-  "+base/android/jni_android.h",
   "+modules/audio_device",
 ]
diff --git a/sdk/android/src/jni/jni_onload.cc b/sdk/android/src/jni/jni_onload.cc
index a1829ad..d23cd92 100644
--- a/sdk/android/src/jni/jni_onload.cc
+++ b/sdk/android/src/jni/jni_onload.cc
@@ -12,6 +12,7 @@
 #undef JNIEXPORT
 #define JNIEXPORT __attribute__((visibility("default")))
 
+#include "rtc_base/logging.h"
 #include "rtc_base/ssl_adapter.h"
 #include "sdk/android/native_api/jni/class_loader.h"
 #include "sdk/android/src/jni/jni_helpers.h"
@@ -20,6 +21,7 @@
 namespace jni {
 
 extern "C" jint JNIEXPORT JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved) {
+  RTC_LOG(LS_INFO) << "Entering JNI_OnLoad in jni_onload.cc";
   jint ret = InitGlobalJniVariables(jvm);
   RTC_DCHECK_GE(ret, 0);
   if (ret < 0)
diff --git a/stats/BUILD.gn b/stats/BUILD.gn
index ab4be89..5a48d85 100644
--- a/stats/BUILD.gn
+++ b/stats/BUILD.gn
@@ -68,11 +68,7 @@
 
     if (is_android) {
       use_default_launcher = false
-      deps += [
-        "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
-        "//testing/android/native_test:native_test_java",
-        "//testing/android/native_test:native_test_support",
-      ]
+      deps += [ "//build/android/gtest_apk:native_test_instrumentation_test_runner_java" ]
     }
   }
 }
diff --git a/system_wrappers/BUILD.gn b/system_wrappers/BUILD.gn
index 1afd20a..786c469 100644
--- a/system_wrappers/BUILD.gn
+++ b/system_wrappers/BUILD.gn
@@ -154,8 +154,6 @@
     ]
 
     if (is_android) {
-      deps += [ "//testing/android/native_test:native_test_support" ]
-
       shard_timeout = 900
     }
   }
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 09bfbf8..bfe3cc3 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -10,6 +10,7 @@
 import("../webrtc.gni")
 if (is_android) {
   import("//build/config/android/rules.gni")
+  import("//third_party/jni_zero/jni_zero.gni")
 }
 
 if (!build_with_chromium) {
@@ -798,7 +799,6 @@
 
       data = test_support_unittests_resources
       if (is_android) {
-        deps += [ "//testing/android/native_test:native_test_support" ]
         shard_timeout = 900
       }
 
@@ -1301,14 +1301,43 @@
   rtc_android_library("native_test_java") {
     testonly = true
     sources = [
+      "android/org/webrtc/native_test/NativeTestWebrtc.java",
+      "android/org/webrtc/native_test/RTCNativeTestApplication.java",
       "android/org/webrtc/native_test/RTCNativeUnitTest.java",
       "android/org/webrtc/native_test/RTCNativeUnitTestActivity.java",
+      "android/org/webrtc/native_test/StrictModeContext.java",
+    ]
+    srcjar_deps = [ ":native_test_jni" ]
+    deps = [
+      ":native_test_support",
+      "../rtc_base:base_java",
+      "//build/android:build_java",
+      "//build/android/gtest_apk:native_test_instrumentation_test_runner_java",
+      "//testing/android/reporter:reporter_java",
+      "//third_party/jni_zero:jni_zero_java",
+    ]
+  }
+
+  source_set("native_test_support") {
+    testonly = true
+    sources = [
+      "android/native_test_launcher.cc",
+      "android/native_test_launcher.h",
+      "android/native_test_util.cc",
+      "android/native_test_util.h",
     ]
     deps = [
-      "../rtc_base:base_java",
-      "//testing/android/native_test:native_test_java",
+      ":native_test_jni",
+      "//testing/gtest",
+      "//third_party/abseil-cpp/absl/strings",
+      "//third_party/jni_zero:jni_zero",
     ]
   }
+
+  generate_jni("native_test_jni") {
+    testonly = true
+    sources = [ "android/org/webrtc/native_test/NativeTestWebrtc.java" ]
+  }
 }
 
 rtc_library("call_config_utils") {
diff --git a/test/DEPS b/test/DEPS
index 95193e7..497c79a 100644
--- a/test/DEPS
+++ b/test/DEPS
@@ -1,6 +1,8 @@
 include_rules = [
   "+third_party/libjpeg",
   "+third_party/libjpeg_turbo",
+  "+third_party/jni_zero",
+  "+absl/strings/str_split.h",
   "+call",
   "+common_audio",
   "+common_video",
diff --git a/test/android/AndroidManifest.xml b/test/android/AndroidManifest.xml
index 04ab33c..478e4dd 100644
--- a/test/android/AndroidManifest.xml
+++ b/test/android/AndroidManifest.xml
@@ -24,13 +24,18 @@
     <uses-permission android:name="android.permission.RECORD_AUDIO"/>
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/> <!-- From Chromium -->
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <!-- From Chromium -->
+    <uses-permission android:name="android.permission.SET_TIME_ZONE"/> <!-- From Chromium -->
 
     <application android:label="NativeTests"
-            android:name="org.chromium.native_test.NativeTestApplication">
+            android:name="org.webrtc.native_test.RTCNativeTestApplication">
         <uses-library android:name="android.test.runner"/>
         <activity android:name=".RTCNativeUnitTestActivity"
                 android:label="NativeTest"
                 android:configChanges="orientation|keyboardHidden"
+                android:requestLegacyExternalStorage="true"
+                android:memtagMode="sync"
                 android:process=":test_process">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/test/android/native_test_launcher.cc b/test/android/native_test_launcher.cc
new file mode 100644
index 0000000..89135a0
--- /dev/null
+++ b/test/android/native_test_launcher.cc
@@ -0,0 +1,123 @@
+/*
+ *  Copyright (c) 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/native_test_launcher.cc
+// and Angle's
+// https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/tests/test_utils/runner/android/AngleNativeTest.cpp
+
+// This class sets up the environment for running the native tests inside an
+// android application. It outputs (to a fifo) markers identifying the
+// START/PASSED/CRASH of the test suite, FAILURE/SUCCESS of individual tests,
+// etc.
+// These markers are read by the test runner script to generate test results.
+// It installs signal handlers to detect crashes.
+
+#include "test/android/native_test_launcher.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "test/android/native_test_util.h"
+#include "test/native_test_jni/NativeTestWebrtc_jni.h"
+#include "third_party/jni_zero/jni_zero.h"
+
+// The main function of the program to be wrapped as a test apk.
+extern int main(int argc, char** argv);
+
+namespace webrtc {
+namespace test {
+namespace android {
+
+namespace {
+
+const char kCrashedMarker[] = "[ CRASHED      ]\n";
+
+// The list of signals which are considered to be crashes.
+const int kExceptionSignals[] = {SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, -1};
+
+struct sigaction g_old_sa[NSIG];
+
+// This function runs in a compromised context. It should not allocate memory.
+void SignalHandler(int sig, siginfo_t* info, void* reserved) {
+  // Output the crash marker.
+  write(STDOUT_FILENO, kCrashedMarker, sizeof(kCrashedMarker) - 1);
+  g_old_sa[sig].sa_sigaction(sig, info, reserved);
+}
+
+}  // namespace
+
+static void JNI_NativeTestWebrtc_RunTests(JNIEnv* env,
+                                          std::string& command_line_flags,
+                                          std::string& command_line_file_path,
+                                          std::string& stdout_file_path,
+                                          std::string& test_data_dir) {
+  AndroidLog(
+      ANDROID_LOG_INFO,
+      "Entering JNI_NativeTestWebrtc_RunTests with command_line_flags=%s, "
+      "command_line_file_path=%s, stdout_file_path=%s, test_data_dir=%s\n",
+      command_line_flags.c_str(), command_line_file_path.c_str(),
+      stdout_file_path.c_str(), test_data_dir.c_str());
+
+  // Required for DEATH_TESTS.
+  pthread_atfork(nullptr, nullptr, jni_zero::DisableJvmForTesting);
+
+  std::vector<std::string> args;
+
+  if (command_line_file_path.empty())
+    args.push_back("_");
+  else
+    ParseArgsFromCommandLineFile(command_line_file_path.c_str(), &args);
+
+  ParseArgsFromString(command_line_flags, &args);
+
+  std::vector<char*> argv;
+  int argc = ArgsToArgv(args, &argv);
+
+  // A few options, such "--gtest_list_tests", will just use printf directly
+  // Always redirect stdout to a known file.
+  if (freopen(stdout_file_path.c_str(), "a+", stdout) == NULL) {
+    AndroidLog(ANDROID_LOG_ERROR, "Failed to redirect stream to file: %s: %s\n",
+               stdout_file_path.c_str(), strerror(errno));
+    exit(EXIT_FAILURE);
+  }
+  // TODO(jbudorick): Remove this after resolving crbug.com/726880
+  AndroidLog(ANDROID_LOG_INFO, "Redirecting stdout to file: %s\n",
+             stdout_file_path.c_str());
+  dup2(STDOUT_FILENO, STDERR_FILENO);
+
+  // TODO(webrtc:42223878): Wait for debugger.
+
+  ScopedMainEntryLogger scoped_main_entry_logger;
+  main(argc, &argv[0]);
+}
+
+// TODO(nileshagrawal): now that we're using FIFO, test scripts can detect EOF.
+// Remove the signal handlers.
+void InstallHandlers() {
+  struct sigaction sa;
+  memset(&sa, 0, sizeof(sa));
+
+  sa.sa_sigaction = SignalHandler;
+  sa.sa_flags = SA_SIGINFO;
+
+  for (unsigned int i = 0; kExceptionSignals[i] != -1; ++i) {
+    sigaction(kExceptionSignals[i], &sa, &g_old_sa[kExceptionSignals[i]]);
+  }
+}
+
+}  // namespace android
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/android/native_test_launcher.h b/test/android/native_test_launcher.h
new file mode 100644
index 0000000..75e4761
--- /dev/null
+++ b/test/android/native_test_launcher.h
@@ -0,0 +1,26 @@
+/*
+ *  Copyright (c) 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.
+ */
+
+#ifndef TEST_ANDROID_NATIVE_TEST_LAUNCHER_H_
+#define TEST_ANDROID_NATIVE_TEST_LAUNCHER_H_
+
+#include <jni.h>
+
+namespace webrtc {
+namespace test {
+namespace android {
+
+void InstallHandlers();
+
+}  // namespace android
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_ANDROID_NATIVE_TEST_LAUNCHER_H_
diff --git a/test/android/native_test_util.cc b/test/android/native_test_util.cc
new file mode 100644
index 0000000..529ffbe
--- /dev/null
+++ b/test/android/native_test_util.cc
@@ -0,0 +1,148 @@
+/*
+ *  Copyright (c) 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/native_test_util.cc
+// and Angle's
+// https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/tests/test_utils/runner/android/AngleNativeTest.cpp
+
+#include "test/android/native_test_util.h"
+
+#include <android/log.h>
+#include <stdio.h>
+
+#include <optional>
+#include <string>
+
+#include "absl/strings/str_split.h"
+#include "absl/strings/string_view.h"
+
+namespace webrtc {
+namespace test {
+namespace android {
+
+namespace {
+
+const char kLogTag[] = "webrtc";
+
+std::optional<std::string> ReadFileToString(const char* path) {
+  FILE* file = fopen(path, "rb");
+  if (!file) {
+    AndroidLog(ANDROID_LOG_ERROR, "Failed to open %s\n", path);
+    return std::nullopt;
+  }
+
+  if (fseek(file, 0, SEEK_END) != 0) {
+    return std::nullopt;
+  }
+
+  auto size = ftell(file);
+  // Check that `size` fits in a 32-bit int, to avoid any issues with overflow
+  // in the subsequent casts. (For example, consider the case where long is >32
+  // bits while size_t is 32 bits.) We're not expecting the command line to be
+  // larger than 1 GB, anyway.
+  if (size < 0 || size > 1'000'000'000) {
+    AndroidLog(ANDROID_LOG_ERROR,
+               "Expected size of %s between 0 and 1 GB, got %ld bytes\n", path,
+               size);
+    return std::nullopt;
+  }
+
+  std::string contents;
+  contents.resize(size);
+
+  fseek(file, 0, SEEK_SET);
+
+  if (fread(contents.data(), 1, size, file) != static_cast<size_t>(size)) {
+    return std::nullopt;
+  }
+
+  if (ferror(file)) {
+    return std::nullopt;
+  }
+
+  return contents;
+}
+
+}  // namespace
+
+// Writes printf() style string to Android's logger where |priority| is one of
+// the levels defined in <android/log.h>.
+void AndroidLog(int priority, const char* format, ...) {
+  va_list args;
+  va_start(args, format);
+  __android_log_vprint(priority, kLogTag, format, args);
+  va_end(args);
+}
+
+std::string ASCIIJavaStringToUTF8(JNIEnv* env, jstring str) {
+  if (!str) {
+    return "";
+  }
+
+  const jsize length = env->GetStringLength(str);
+  if (!length) {
+    return "";
+  }
+
+  // JNI's GetStringUTFChars() returns strings in Java "modified" UTF8, so
+  // instead get the String in UTF16. As the input is ASCII, drop the higher
+  // bytes.
+  const jchar* jchars = env->GetStringChars(str, NULL);
+  const char16_t* chars = reinterpret_cast<const char16_t*>(jchars);
+  std::string out(chars, chars + length);
+  env->ReleaseStringChars(str, jchars);
+  return out;
+}
+
+void ParseArgsFromString(const std::string& command_line,
+                         std::vector<std::string>* args) {
+  std::vector<absl::string_view> v =
+      absl::StrSplit(command_line, absl::ByAsciiWhitespace());
+  for (absl::string_view arg : v) {
+    args->push_back(std::string(arg));
+  }
+
+  // TODO(webrtc:42223878): Implement tokenization that handle quotes and
+  // escaped quotes (along the lines of the previous chromium code):
+  //
+  // base::StringTokenizer tokenizer(command_line, base::kWhitespaceASCII);
+  // tokenizer.set_quote_chars("\"");
+  // while (tokenizer.GetNext()) {
+  //   std::string token;
+  //   base::RemoveChars(tokenizer.token(), "\"", &token);
+  //   args->push_back(token);
+  // }
+}
+
+void ParseArgsFromCommandLineFile(const char* path,
+                                  std::vector<std::string>* args) {
+  std::optional<std::string> command_line_string = ReadFileToString(path);
+  if (command_line_string.has_value()) {
+    ParseArgsFromString(*command_line_string, args);
+  }
+}
+
+int ArgsToArgv(const std::vector<std::string>& args, std::vector<char*>* argv) {
+  // We need to pass in a non-const char**.
+  int argc = args.size();
+
+  argv->resize(argc + 1);
+  for (int i = 0; i < argc; ++i) {
+    (*argv)[i] = const_cast<char*>(args[i].c_str());
+  }
+  (*argv)[argc] = NULL;  // argv must be NULL terminated.
+
+  return argc;
+}
+
+}  // namespace android
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/android/native_test_util.h b/test/android/native_test_util.h
new file mode 100644
index 0000000..ace8758
--- /dev/null
+++ b/test/android/native_test_util.h
@@ -0,0 +1,63 @@
+/*
+ *  Copyright (c) 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.
+ */
+
+#ifndef TEST_ANDROID_NATIVE_TEST_UTIL_H_
+#define TEST_ANDROID_NATIVE_TEST_UTIL_H_
+
+#include <android/log.h>
+
+#include <string>
+#include <vector>
+
+#include "third_party/jni_zero/jni_zero.h"
+
+// Helper methods for setting up environment for running gtest tests
+// inside an APK.
+namespace webrtc {
+namespace test {
+namespace android {
+
+void AndroidLog(int priority, const char* format, ...);
+
+std::string ASCIIJavaStringToUTF8(JNIEnv* env, jstring str);
+
+void ParseArgsFromString(const std::string& command_line,
+                         std::vector<std::string>* args);
+void ParseArgsFromCommandLineFile(const char* path,
+                                  std::vector<std::string>* args);
+int ArgsToArgv(const std::vector<std::string>& args, std::vector<char*>* argv);
+
+class ScopedMainEntryLogger {
+ public:
+  ScopedMainEntryLogger() {
+    AndroidLog(ANDROID_LOG_INFO, ">>ScopedMainEntryLogger\n");
+  }
+
+  ~ScopedMainEntryLogger() {
+    AndroidLog(ANDROID_LOG_INFO, "<<ScopedMainEntryLogger\n");
+    fflush(stdout);
+    fflush(stderr);
+  }
+};
+
+}  // namespace android
+}  // namespace test
+}  // namespace webrtc
+
+namespace jni_zero {
+template <>
+inline std::string FromJniType<std::string>(JNIEnv* env,
+                                            const JavaRef<jobject>& input) {
+  return webrtc::test::android::ASCIIJavaStringToUTF8(
+      env, static_cast<jstring>(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 0000000..352f9c8
--- /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 0000000..ce2cf4f
--- /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 dede7ed..329e1e8 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 0000000..24e036c
--- /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.
+ *
+ * <p>Prefer "ignored" as the variable name to appease Android Studio's "Unused symbol" inspection.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ *     try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
+ *         return Example.doThingThatRequiresDiskWrites();
+ *     }
+ * </pre>
+ */
+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 6a09451..ba5c39d 100644
--- a/webrtc.gni
+++ b/webrtc.gni
@@ -525,13 +525,17 @@
       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",
       ]
     }