From 4ab1e605293ec2a00b59c18913910d901ace5342 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Mon, 27 Mar 2023 01:09:07 +0800
Subject: [PATCH] Create AudioRecord by reflection as a fallback

Some devices (Vivo phones) fail to create an AudioRecord from an
AudioRecord.Builder (which throw a NullPointerException).

In that case, create an AudioRecord instance directly by reflection.

The AOSP version of AudioRecord constructor code can be found at:
 - Android 11 (R):
   <https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=335;drc=64ed2ec38a511bbbd048985fe413268335e072f8>
 - Android 12 (S):
   <https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=388;drc=2eebf929650e0d320a21f0d13677a27d7ab278e9>
 - Android 13 (T, functionally identical to Android 12):
   <https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=382;drc=ed242da52f975a1dd18671afb346b18853d729f2>
 - Android 14 (U): Not released, but expected to change

PR #3862 <https://github.com/Genymobile/scrcpy/pull/3862>
Fixes #3805 <https://github.com/Genymobile/scrcpy/issues/3805>

Signed-off-by: Romain Vimont <rom@rom1v.com>
---
 .../com/genymobile/scrcpy/AudioCapture.java   |  10 +-
 .../com/genymobile/scrcpy/Workarounds.java    | 145 ++++++++++++++++++
 2 files changed, 154 insertions(+), 1 deletion(-)

diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
index 92ecd839..b8fc076b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
@@ -22,6 +22,7 @@ public final class AudioCapture {
     public static final int SAMPLE_RATE = 48000;
     public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
     public static final int CHANNELS = 2;
+    public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
     public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
     public static final int BYTES_PER_SAMPLE = 2;
 
@@ -98,7 +99,14 @@ public final class AudioCapture {
     }
 
     private void startRecording() {
-        recorder = createAudioRecord();
+        try {
+            recorder = createAudioRecord();
+        } catch (NullPointerException e) {
+            // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
+            // - <https://github.com/Genymobile/scrcpy/issues/3805>
+            // - <https://github.com/Genymobile/scrcpy/pull/3862>
+            recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
+        }
         recorder.startRecording();
     }
 
diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
index 89380ece..b343a344 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
@@ -1,13 +1,22 @@
 package com.genymobile.scrcpy;
 
 import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
 import android.app.Application;
+import android.content.AttributionSource;
 import android.content.ContextWrapper;
 import android.content.pm.ApplicationInfo;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.os.Build;
 import android.os.Looper;
+import android.os.Parcel;
 
+import java.lang.ref.WeakReference;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 
 public final class Workarounds {
 
@@ -95,4 +104,140 @@ public final class Workarounds {
             Ln.d("Could not fill app context: " + throwable.getMessage());
         }
     }
+
+    @TargetApi(Build.VERSION_CODES.R)
+    @SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
+    public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
+        // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
+        //
+        // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses
+        // reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do).
+        // As a result, the modified code was not executed.
+        try {
+            // AudioRecord audioRecord = new AudioRecord(0L);
+            Constructor<AudioRecord> audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class);
+            audioRecordConstructor.setAccessible(true);
+            AudioRecord audioRecord = audioRecordConstructor.newInstance(0L);
+
+            // audioRecord.mRecordingState = RECORDSTATE_STOPPED;
+            Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState");
+            mRecordingStateField.setAccessible(true);
+            mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED);
+
+            Looper looper = Looper.myLooper();
+            if (looper == null) {
+                looper = Looper.getMainLooper();
+            }
+
+            // audioRecord.mInitializationLooper = looper;
+            Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper");
+            mInitializationLooperField.setAccessible(true);
+            mInitializationLooperField.set(audioRecord, looper);
+
+            // Create `AudioAttributes` with fixed capture preset
+            int capturePreset = source;
+            AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
+            Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class);
+            setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset);
+            AudioAttributes attributes = audioAttributesBuilder.build();
+
+            // audioRecord.mAudioAttributes = attributes;
+            Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes");
+            mAudioAttributesField.setAccessible(true);
+            mAudioAttributesField.set(audioRecord, attributes);
+
+            // audioRecord.audioParamCheck(capturePreset, sampleRate, encoding);
+            Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class);
+            audioParamCheckMethod.setAccessible(true);
+            audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding);
+
+            // audioRecord.mChannelCount = channels
+            Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount");
+            mChannelCountField.setAccessible(true);
+            mChannelCountField.set(audioRecord, channels);
+
+            // audioRecord.mChannelMask = channelMask
+            Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask");
+            mChannelMaskField.setAccessible(true);
+            mChannelMaskField.set(audioRecord, channelMask);
+
+            int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding);
+            int bufferSizeInBytes = minBufferSize * 8;
+
+            // audioRecord.audioBuffSizeCheck(bufferSizeInBytes)
+            Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class);
+            audioBuffSizeCheckMethod.setAccessible(true);
+            audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes);
+
+            final int channelIndexMask = 0;
+
+            int[] sampleRateArray = new int[]{sampleRate};
+            int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
+
+            int initResult;
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+                // private native final int native_setup(Object audiorecord_this,
+                // Object /*AudioAttributes*/ attributes,
+                // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
+                // int buffSizeInBytes, int[] sessionId, String opPackageName,
+                // long nativeRecordInJavaObj);
+                Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
+                        int.class, int.class, int.class, int[].class, String.class, long.class);
+                nativeSetupMethod.setAccessible(true);
+                initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
+                        channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(),
+                        0L);
+            } else {
+                // Assume `context` is never `null`
+                AttributionSource attributionSource = FakeContext.get().getAttributionSource();
+
+                // Assume `attributionSource.getPackageName()` is never null
+
+                // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()
+                Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState");
+                asScopedParcelStateMethod.setAccessible(true);
+
+                try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) {
+                    Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
+                    Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
+
+                    // private native int native_setup(Object audiorecordThis,
+                    // Object /*AudioAttributes*/ attributes,
+                    // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
+                    // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
+                    // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
+                    Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
+                            int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
+                    nativeSetupMethod.setAccessible(true);
+                    initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
+                            channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
+                }
+            }
+
+            if (initResult != AudioRecord.SUCCESS) {
+                Ln.e("Error code " + initResult + " when initializing native AudioRecord object.");
+                throw new RuntimeException("Cannot create AudioRecord");
+            }
+
+            // mSampleRate = sampleRate[0]
+            Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate");
+            mSampleRateField.setAccessible(true);
+            mSampleRateField.set(audioRecord, sampleRateArray[0]);
+
+            // audioRecord.mSessionId = session[0]
+            Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId");
+            mSessionIdField.setAccessible(true);
+            mSessionIdField.set(audioRecord, session[0]);
+
+            // audioRecord.mState = AudioRecord.STATE_INITIALIZED
+            Field mStateField = AudioRecord.class.getDeclaredField("mState");
+            mStateField.setAccessible(true);
+            mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED);
+
+            return audioRecord;
+        } catch (Exception e) {
+            Ln.e("Failed to invoke AudioRecord.<init>.", e);
+            throw new RuntimeException("Cannot create AudioRecord");
+        }
+    }
 }