Adding thread checker for audio record on native APIs

Implemented thread safety checks in WebRtcAudioRecord.java using ThreadUtils.ThreadChecker, matching the pattern in WebRtcAudioTrack.java. The checker is detached in the constructor and guards @CalledByNative control APIs (initRecording, startRecording, stopRecording, enableBuiltInAEC, enableBuiltInNS) to ensure they are called consistently from the same C++ worker thread. To prevent crashes, Java-only public APIs (such as setPreferredDevice and setMicrophoneMute) and read-only query APIs remain unguarded as they are expected to be called from application or stats threads.

Bug: webrtc:524395191
Change-Id: I432b9f6f8da1ead5e3ff021dfef4c6e12ef6557f
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/484740
Reviewed-by: Henrik Lundin <henrik.lundin@webrtc.org>
Reviewed-by: Henrik Andreassson <henrika@webrtc.org>
Commit-Queue: Tim Na <natim@google.com>
Cr-Commit-Position: refs/heads/main@{#48068}
diff --git a/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java b/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java
index 510cc04..26a1bfa 100644
--- a/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java
+++ b/sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java
@@ -107,6 +107,7 @@
   private final @Nullable SamplesReadyCallback audioSamplesReadyCallback;
   private final boolean isAcousticEchoCancelerSupported;
   private final boolean isNoiseSuppressorSupported;
+  private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker();
 
   /**
    * Audio thread which keeps calling ByteBuffer.read() waiting for audio
@@ -207,6 +208,7 @@
       @Nullable AudioRecordStateCallback stateCallback,
       @Nullable SamplesReadyCallback audioSamplesReadyCallback,
       boolean isAcousticEchoCancelerSupported, boolean isNoiseSuppressorSupported) {
+    threadChecker.detachThread();
     if (isAcousticEchoCancelerSupported && !WebRtcAudioEffects.isAcousticEchoCancelerSupported()) {
       throw new IllegalArgumentException("HW AEC not supported");
     }
@@ -264,18 +266,27 @@
 
   @CalledByNative
   private boolean enableBuiltInAEC(boolean enable) {
+    threadChecker.checkIsOnValidThread();
     Logging.d(TAG, "enableBuiltInAEC(" + enable + ")");
     return effects.setAEC(enable);
   }
 
   @CalledByNative
   private boolean enableBuiltInNS(boolean enable) {
+    threadChecker.checkIsOnValidThread();
     Logging.d(TAG, "enableBuiltInNS(" + enable + ")");
     return effects.setNS(enable);
   }
 
   @CalledByNative
   private int initRecording(int sampleRate, int channels) {
+    try {
+      threadChecker.checkIsOnValidThread();
+    } catch (IllegalStateException e) {
+      reportWebRtcAudioRecordInitError("threadChecker.checkIsOnValidThread failed: " +
+                                       e.getMessage());
+      return -1;
+    }
     Logging.d(TAG, "initRecording(sampleRate=" + sampleRate + ", channels=" + channels + ")");
     if (audioRecord != null) {
       reportWebRtcAudioRecordInitError("InitRecording called twice without StopRecording.");
@@ -384,6 +395,7 @@
 
   @CalledByNative
   private boolean startRecording() {
+    threadChecker.checkIsOnValidThread();
     Logging.d(TAG, "startRecording");
     assertTrue(audioRecord != null);
     assertTrue(audioThread == null);
@@ -408,6 +420,7 @@
 
   @CalledByNative
   private boolean stopRecording() {
+    threadChecker.checkIsOnValidThread();
     Logging.d(TAG, "stopRecording");
     assertTrue(audioThread != null);
     if (future != null) {