From 92e7bcb2fb69641065e74046c56e1f74dbb4b226 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 1/3] Use reflection to create AudioRecord
---
server/build.gradle | 6 +
server/lint-baseline.xml | 48 +++
.../com/genymobile/scrcpy/AudioCapture.java | 8 +-
.../wrappers/AudioAttributesWrapper.java | 135 ++++++
.../scrcpy/wrappers/AudioFormatWrapper.java | 78 ++++
.../scrcpy/wrappers/AudioRecordWrapper.java | 383 ++++++++++++++++++
6 files changed, 657 insertions(+), 1 deletion(-)
create mode 100644 server/lint-baseline.xml
create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
diff --git a/server/build.gradle b/server/build.gradle
index ce234d10f1..c5925a9e02 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -24,3 +24,9 @@ dependencies {
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"
+
+android {
+ lintOptions {
+ baseline file("lint-baseline.xml")
+ }
+}
diff --git a/server/lint-baseline.xml b/server/lint-baseline.xml
new file mode 100644
index 0000000000..c0045ef96f
--- /dev/null
+++ b/server/lint-baseline.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
index c940db167c..b545cf1d7e 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
@@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
+import com.genymobile.scrcpy.wrappers.AudioRecordWrapper;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
@@ -55,7 +56,12 @@ private static AudioRecord createAudioRecord() {
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
- return builder.build();
+
+ try {
+ return builder.build();
+ } catch (Exception e) {
+ return AudioRecordWrapper.build(builder);
+ }
}
private static void startWorkaroundAndroid11() {
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
new file mode 100644
index 0000000000..283fee1726
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
@@ -0,0 +1,135 @@
+package com.genymobile.scrcpy.wrappers;
+
+import com.genymobile.scrcpy.Ln;
+
+import android.media.AudioAttributes;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class AudioAttributesWrapper {
+ private static Method getCapturePresetMethod;
+ private static Method setCapturePresetMethod;
+ private static Method getTagsMethod;
+
+ private static Method getGetCapturePresetMethod() throws NoSuchMethodException {
+ if (getCapturePresetMethod == null) {
+ getCapturePresetMethod = AudioAttributes.class.getMethod("getCapturePreset");
+ }
+ return getCapturePresetMethod;
+ }
+
+ public static int getCapturePreset(AudioAttributes audioAttributes)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
+ try {
+ return (int) getGetCapturePresetMethod().invoke(audioAttributes);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.getCapturePreset()", e);
+ throw e;
+ }
+ }
+
+ private static Method getSetCapturePresetMethod() throws NoSuchMethodException {
+ if (setCapturePresetMethod == null) {
+ setCapturePresetMethod = AudioAttributes.class.getMethod("setCapturePreset", int.class);
+ }
+ return setCapturePresetMethod;
+ }
+
+ public static void setCapturePreset(AudioAttributes audioAttributes, int preset)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
+ try {
+ getSetCapturePresetMethod().invoke(audioAttributes, preset);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.setCapturePreset()", e);
+ throw e;
+ }
+ }
+
+ private static Method getGetTagsMethod() throws NoSuchMethodException {
+ if (getTagsMethod == null) {
+ getTagsMethod = AudioAttributes.class.getMethod("getTags");
+ }
+ return getTagsMethod;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Set getTags(AudioAttributes audioAttributes)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
+ try {
+ return (Set) getGetTagsMethod().invoke(audioAttributes);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.getTags()", e);
+ throw e;
+ }
+ }
+
+ public static class Builder {
+ private static Method setInternalCapturePresetMethod;
+ private static Method setPrivacySensitiveMethod;
+ private static Method addTagMethod;
+
+ private static Method getSetInternalCapturePresetMethod() throws NoSuchMethodException {
+ if (setInternalCapturePresetMethod == null) {
+ setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset",
+ int.class);
+ }
+ return setInternalCapturePresetMethod;
+ }
+
+ public static void setInternalCapturePreset(AudioAttributes.Builder builder, int preset)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ getSetInternalCapturePresetMethod().invoke(builder, preset);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.Builder.setInternalCapturePreset()", e);
+ throw e;
+ }
+ }
+
+ private static Method getSetPrivacySensitiveMethod() throws NoSuchMethodException {
+ if (setPrivacySensitiveMethod == null) {
+ setPrivacySensitiveMethod = AudioAttributes.Builder.class.getMethod("setPrivacySensitive",
+ boolean.class);
+ }
+ return setPrivacySensitiveMethod;
+ }
+
+ public static void setPrivacySensitive(AudioAttributes.Builder builder, boolean privacySensitive)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ getSetPrivacySensitiveMethod().invoke(builder, privacySensitive);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.Builder.setPrivacySensitive()", e);
+ throw e;
+ }
+ }
+
+ private static Method getAddTagMethod() throws NoSuchMethodException {
+ if (addTagMethod == null) {
+ addTagMethod = AudioAttributes.Builder.class.getMethod("addTag", String.class);
+ }
+ return addTagMethod;
+ }
+
+ public static void addTag(AudioAttributes.Builder builder, String tag)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ getAddTagMethod().invoke(builder, tag);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioAttributes.Builder.addTag()", e);
+ throw e;
+ }
+ }
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
new file mode 100644
index 0000000000..71aa5cbe93
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
@@ -0,0 +1,78 @@
+package com.genymobile.scrcpy.wrappers;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import com.genymobile.scrcpy.Ln;
+
+import android.media.AudioFormat;
+
+public class AudioFormatWrapper {
+ public final static int AUDIO_FORMAT_HAS_PROPERTY_NONE = 0x0;
+ public final static int AUDIO_FORMAT_HAS_PROPERTY_ENCODING = 0x1 << 0;
+ public final static int AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE = 0x1 << 1;
+ public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK = 0x1 << 2;
+ public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK = 0x1 << 3;
+
+ private static Method getPropertySetMaskMethod;
+ private static Method channelCountFromInChannelMaskMethod;
+ private static Method getBytesPerSampleMethod;
+
+ private static Method getGetPropertySetMaskMethod() throws NoSuchMethodException {
+ if (getPropertySetMaskMethod == null) {
+ getPropertySetMaskMethod = AudioFormat.class.getMethod("getPropertySetMask");
+ }
+ return getPropertySetMaskMethod;
+ }
+
+ public static int getPropertySetMask(AudioFormat audioFormat)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ return (int) getGetPropertySetMaskMethod().invoke(audioFormat);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioFormat.getPropertySetMask()", e);
+ throw e;
+ }
+ }
+
+ private static Method getChannelCountFromInChannelMaskMethod() throws NoSuchMethodException {
+ if (channelCountFromInChannelMaskMethod == null) {
+ channelCountFromInChannelMaskMethod = AudioFormat.class.getMethod("channelCountFromInChannelMask",
+ int.class);
+ }
+ return channelCountFromInChannelMaskMethod;
+ }
+
+ public static int channelCountFromInChannelMask(int channelMask)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ return (int) getChannelCountFromInChannelMaskMethod().invoke(null, channelMask);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioFormat.channelCountFromInChannelMask()", e);
+ throw e;
+ }
+ }
+
+ private static Method getGetBytesPerSampleMethod() throws NoSuchMethodException {
+ if (getBytesPerSampleMethod == null) {
+ getBytesPerSampleMethod = AudioFormat.class.getMethod("getBytesPerSample", int.class);
+ }
+ return getBytesPerSampleMethod;
+ }
+
+ public static int getBytesPerSample(AudioFormat format, int encoding)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ return (int) getGetBytesPerSampleMethod().invoke(format, encoding);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioFormat.getBytesPerSample()", e);
+ throw e;
+ }
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
new file mode 100644
index 0000000000..942ea88138
--- /dev/null
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
@@ -0,0 +1,383 @@
+package com.genymobile.scrcpy.wrappers;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+
+import com.genymobile.scrcpy.Ln;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Looper;
+import android.os.Parcel;
+
+public class AudioRecordWrapper {
+ public final static String SUBMIX_FIXED_VOLUME = "fixedVolume";
+
+ private static Method getChannelMaskFromLegacyConfigMethod;
+ private static Method getCurrentOpPackageNameMethod;
+
+ private static Method getGetChannelMaskFromLegacyConfigMethod() throws NoSuchMethodException {
+ if (getChannelMaskFromLegacyConfigMethod == null) {
+ getChannelMaskFromLegacyConfigMethod = AudioRecord.class.getDeclaredMethod("getChannelMaskFromLegacyConfig",
+ int.class, boolean.class);
+ getChannelMaskFromLegacyConfigMethod.setAccessible(true);
+ }
+ return getChannelMaskFromLegacyConfigMethod;
+ }
+
+ public static int getChannelMaskFromLegacyConfig(int inChannelConfig, boolean allowLegacyConfig)
+ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
+ NoSuchMethodException {
+ try {
+ return (int) getGetChannelMaskFromLegacyConfigMethod().invoke(null, inChannelConfig, allowLegacyConfig);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioRecord.getChannelMaskFromLegacyConfig()", e);
+ return 0;
+ }
+ }
+
+ private static Method getGetCurrentOpPackageNameMethod() throws NoSuchMethodException {
+ if (getCurrentOpPackageNameMethod == null) {
+ getCurrentOpPackageNameMethod = AudioRecord.class.getDeclaredMethod("getCurrentOpPackageName");
+ getCurrentOpPackageNameMethod.setAccessible(true);
+ }
+ return getCurrentOpPackageNameMethod;
+ }
+
+ public static String getCurrentOpPackageName(AudioRecord audioRecord)
+ throws IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException, NoSuchMethodException {
+ try {
+ return (String) getGetCurrentOpPackageNameMethod().invoke(audioRecord);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | NoSuchMethodException e) {
+ Ln.e("Failed to invoke AudioRecord.getCurrentOpPackageName()", e);
+ return null;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.R)
+ @SuppressLint({ "WrongConstant", "MissingPermission" })
+ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat format,
+ int bufferSizeInBytes, int sessionId, Context context, int maxSharedAudioHistoryMs)
+ throws Exception {
+ // 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.
+ //
+ // 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
+
+ try {
+ // AudioRecord audioRecord = new AudioRecord(0L);
+ Constructor 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);
+
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ if (format == null) {
+ throw new IllegalArgumentException("Illegal null AudioFormat");
+ }
+
+ 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);
+
+ if (AudioAttributesWrapper.getCapturePreset(attributes) == MediaRecorder.AudioSource.REMOTE_SUBMIX) {
+ AudioAttributes.Builder audioAttributeBuilder = new AudioAttributes.Builder(attributes);
+
+ final Iterator tagsIter = AudioAttributesWrapper.getTags(attributes).iterator();
+ while (tagsIter.hasNext()) {
+ String tag = tagsIter.next();
+ if (tag.equalsIgnoreCase(SUBMIX_FIXED_VOLUME)) {
+ // audioRecord.mIsSubmixFullVolume = true;
+ Field mIsSubmixFullVolumeField = AudioRecord.class.getDeclaredField("mIsSubmixFullVolume");
+ mIsSubmixFullVolumeField.setAccessible(true);
+ mIsSubmixFullVolumeField.set(audioRecord, true);
+ } else {
+ AudioAttributesWrapper.Builder.addTag(audioAttributeBuilder, tag);
+ }
+ }
+
+ AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributeBuilder,
+ AudioAttributesWrapper.getCapturePreset(attributes));
+ attributes = audioAttributeBuilder.build();
+ }
+
+ // audioRecord.mAudioAttributes = attributes;
+ Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes");
+ mAudioAttributesField.setAccessible(true);
+ mAudioAttributesField.set(audioRecord, attributes);
+
+ int rate = format.getSampleRate();
+ if (rate == AudioFormat.SAMPLE_RATE_UNSPECIFIED) {
+ rate = 0;
+ }
+
+ int encoding = AudioFormat.ENCODING_DEFAULT;
+ if ((AudioFormatWrapper.getPropertySetMask(format)
+ & AudioFormatWrapper.AUDIO_FORMAT_HAS_PROPERTY_ENCODING) != 0) {
+ encoding = format.getEncoding();
+ }
+
+ // audioRecord.audioParamCheck(capturePreset, rate, encoding);
+ Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class,
+ int.class);
+ audioParamCheckMethod.setAccessible(true);
+ audioParamCheckMethod.invoke(audioRecord, AudioAttributesWrapper.getCapturePreset(attributes), rate,
+ encoding);
+
+ int mChannelIndexMask = 0;
+ int mChannelMask = 0;
+ int mChannelCount = 0;
+
+ if ((AudioFormatWrapper.getPropertySetMask(format)
+ & AudioFormatWrapper.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK) != 0) {
+ mChannelIndexMask = format.getChannelIndexMask();
+ mChannelCount = format.getChannelCount();
+ }
+ if ((AudioFormatWrapper.getPropertySetMask(format)
+ & AudioFormatWrapper.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK) != 0) {
+ mChannelMask = getChannelMaskFromLegacyConfig(format.getChannelMask(), false);
+ mChannelCount = format.getChannelCount();
+ } else {
+ mChannelMask = getChannelMaskFromLegacyConfig(AudioFormat.CHANNEL_IN_DEFAULT, false);
+ mChannelCount = AudioFormatWrapper.channelCountFromInChannelMask(mChannelMask);
+ }
+
+ Field mChannelIndexMaskField = AudioRecord.class.getDeclaredField("mChannelIndexMask");
+ mChannelIndexMaskField.setAccessible(true);
+ mChannelIndexMaskField.set(audioRecord, mChannelIndexMask);
+
+ Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask");
+ mChannelMaskField.setAccessible(true);
+ mChannelMaskField.set(audioRecord, mChannelMask);
+
+ Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount");
+ mChannelCountField.setAccessible(true);
+ mChannelCountField.set(audioRecord, mChannelCount);
+
+ // audioRecord.audioBuffSizeCheck(bufferSizeInBytes)
+ Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class);
+ audioBuffSizeCheckMethod.setAccessible(true);
+ audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes);
+
+ int[] sampleRate = new int[] { 0 };
+ int[] session = new int[] { sessionId };
+
+ 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),
+ attributes, sampleRate, mChannelMask, mChannelIndexMask, audioRecord.getAudioFormat(),
+ bufferSizeInBytes, session, getCurrentOpPackageName(audioRecord), 0L);
+ } else {
+ AttributionSource attributionSource = context != null ? context.getAttributionSource()
+ : AttributionSource.myAttributionSource();
+
+ if (attributionSource.getPackageName() == null) {
+ // Command line utility
+ // attributionSource = attributionSource.withPackageName("uid:" +
+ // Binder.getCallingUid());
+ Method withPackageNameMethod = AttributionSource.class.getDeclaredMethod("withPackageName",
+ String.class);
+ withPackageNameMethod.setAccessible(true);
+ attributionSource = (AttributionSource) withPackageNameMethod.invoke(attributionSource,
+ "uid:" + Binder.getCallingUid());
+ }
+
+ // 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), attributes, sampleRate, mChannelMask,
+ mChannelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
+ attributionSourceParcel, 0L, maxSharedAudioHistoryMs);
+ }
+ }
+
+ if (initResult != AudioRecord.SUCCESS) {
+ Ln.e("Error code " + initResult + " when initializing native AudioRecord object.");
+ return audioRecord;
+ }
+
+ // mSampleRate = sampleRate[0]
+ Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate");
+ mSampleRateField.setAccessible(true);
+ mSampleRateField.set(audioRecord, sampleRate[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..", e);
+ throw e;
+ }
+ }
+
+ public static final int PRIVACY_SENSITIVE_DEFAULT = -1;
+ public static final int PRIVACY_SENSITIVE_DISABLED = 0;
+ public static final int PRIVACY_SENSITIVE_ENABLED = 1;
+
+ public static AudioRecord build(AudioRecord.Builder builder) {
+ try {
+ Field mFormatField = AudioRecord.Builder.class.getDeclaredField("mFormat");
+ mFormatField.setAccessible(true);
+ AudioFormat mFormat = (AudioFormat) mFormatField.get(builder);
+
+ if (mFormat == null) {
+ mFormat = new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
+ .build();
+ } else {
+ if (mFormat.getEncoding() == AudioFormat.ENCODING_INVALID) {
+ mFormat = new AudioFormat.Builder(mFormat)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .build();
+ }
+ if (mFormat.getChannelMask() == AudioFormat.CHANNEL_INVALID
+ && mFormat.getChannelIndexMask() == AudioFormat.CHANNEL_INVALID) {
+ mFormat = new AudioFormat.Builder(mFormat)
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
+ .build();
+ }
+ }
+
+ Field mAttributesField = AudioRecord.Builder.class.getDeclaredField("mAttributes");
+ mAttributesField.setAccessible(true);
+ AudioAttributes mAttributes = (AudioAttributes) mAttributesField.get(builder);
+ if (mAttributes == null) {
+ AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
+ AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributesBuilder,
+ MediaRecorder.AudioSource.DEFAULT);
+ mAttributes = audioAttributesBuilder.build();
+ }
+
+ Field mPrivacySensitiveField = AudioRecord.Builder.class.getDeclaredField("mPrivacySensitive");
+ mPrivacySensitiveField.setAccessible(true);
+ int mPrivacySensitive = (int) mPrivacySensitiveField.get(builder);
+ if (mPrivacySensitive != PRIVACY_SENSITIVE_DEFAULT) {
+ int source = AudioAttributesWrapper.getCapturePreset(mAttributes);
+ if (source == MediaRecorder.AudioSource.REMOTE_SUBMIX
+ || source == MediaRecorder.AudioSource.VOICE_DOWNLINK
+ || source == MediaRecorder.AudioSource.VOICE_UPLINK
+ || source == MediaRecorder.AudioSource.VOICE_CALL) {
+ throw new UnsupportedOperationException(
+ "Cannot request private capture with source: " + source);
+ }
+
+ AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
+ AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributesBuilder, source);
+ AudioAttributesWrapper.Builder.setPrivacySensitive(audioAttributesBuilder,
+ mPrivacySensitive == PRIVACY_SENSITIVE_ENABLED);
+ mAttributes = audioAttributesBuilder.build();
+ }
+
+ Field mBufferSizeInBytesField = AudioRecord.Builder.class.getDeclaredField("mBufferSizeInBytes");
+ mBufferSizeInBytesField.setAccessible(true);
+ int mBufferSizeInBytes = (int) mBufferSizeInBytesField.get(builder);
+
+ // If the buffer size is not specified,
+ // use a single frame for the buffer size and let the
+ // native code figure out the minimum buffer size.
+ if (mBufferSizeInBytes == 0) {
+ mBufferSizeInBytes = mFormat.getChannelCount()
+ * AudioFormatWrapper.getBytesPerSample(mFormat, mFormat.getEncoding());
+ }
+
+ Field mSessionIdField = AudioRecord.Builder.class.getDeclaredField("mSessionId");
+ mSessionIdField.setAccessible(true);
+ int mSessionId = (int) mSessionIdField.get(builder);
+
+ Context mContext;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Field mContextField = AudioRecord.Builder.class.getDeclaredField("mContext");
+ mContextField.setAccessible(true);
+ mContext = (Context) mContextField.get(builder);
+ } else {
+ mContext = null;
+ }
+
+ final AudioRecord record = newInstance(
+ mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext, 0);
+ if (record.getState() == AudioRecord.STATE_UNINITIALIZED) {
+ // release is not necessary
+ throw new UnsupportedOperationException("Cannot create AudioRecord");
+ }
+ return record;
+ } catch (Exception e) {
+ throw new UnsupportedOperationException("Cannot create AudioRecord", e);
+ }
+ }
+}
From b3ca4d95df611c6ce2d823cbf64fccd2406e7738 Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Tue, 4 Apr 2023 22:57:47 +0800
Subject: [PATCH 2/3] Use SuppressLint annotation
---
server/build.gradle | 6 ---
server/lint-baseline.xml | 48 -------------------
.../scrcpy/wrappers/AudioRecordWrapper.java | 3 +-
3 files changed, 2 insertions(+), 55 deletions(-)
delete mode 100644 server/lint-baseline.xml
diff --git a/server/build.gradle b/server/build.gradle
index c5925a9e02..ce234d10f1 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -24,9 +24,3 @@ dependencies {
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"
-
-android {
- lintOptions {
- baseline file("lint-baseline.xml")
- }
-}
diff --git a/server/lint-baseline.xml b/server/lint-baseline.xml
deleted file mode 100644
index c0045ef96f..0000000000
--- a/server/lint-baseline.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
index 942ea88138..197eb7929e 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
@@ -70,7 +70,7 @@ public static String getCurrentOpPackageName(AudioRecord audioRecord)
}
@TargetApi(Build.VERSION_CODES.R)
- @SuppressLint({ "WrongConstant", "MissingPermission" })
+ @SuppressLint({ "WrongConstant", "MissingPermission", "BlockedPrivateApi" })
public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat format,
int bufferSizeInBytes, int sessionId, Context context, int maxSharedAudioHistoryMs)
throws Exception {
@@ -289,6 +289,7 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
public static final int PRIVACY_SENSITIVE_DISABLED = 0;
public static final int PRIVACY_SENSITIVE_ENABLED = 1;
+ @SuppressLint({ "BlockedPrivateApi" })
public static AudioRecord build(AudioRecord.Builder builder) {
try {
Field mFormatField = AudioRecord.Builder.class.getDeclaredField("mFormat");
From c4aea69d788238cebb49aac718c8a9fbef090f9b Mon Sep 17 00:00:00 2001
From: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Date: Tue, 18 Apr 2023 22:21:12 +0800
Subject: [PATCH 3/3] Remove some checks
---
.../com/genymobile/scrcpy/AudioCapture.java | 9 +-
.../wrappers/AudioAttributesWrapper.java | 135 ------------
.../scrcpy/wrappers/AudioFormatWrapper.java | 43 ----
.../scrcpy/wrappers/AudioRecordWrapper.java | 195 +++---------------
4 files changed, 39 insertions(+), 343 deletions(-)
delete mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
index b545cf1d7e..d6b0ba4745 100644
--- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
+++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
@@ -51,16 +51,17 @@ private static AudioRecord createAudioRecord() {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
+ AudioFormat format = createAudioFormat();
+ int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT) * 8;
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
- builder.setAudioFormat(createAudioFormat());
- int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
+ builder.setAudioFormat(format);
// This buffer size does not impact latency
- builder.setBufferSizeInBytes(8 * minBufferSize);
+ builder.setBufferSizeInBytes(minBufferSize);
try {
return builder.build();
} catch (Exception e) {
- return AudioRecordWrapper.build(builder);
+ return AudioRecordWrapper.newInstance(format, minBufferSize, FakeContext.get());
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
deleted file mode 100644
index 283fee1726..0000000000
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioAttributesWrapper.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.genymobile.scrcpy.wrappers;
-
-import com.genymobile.scrcpy.Ln;
-
-import android.media.AudioAttributes;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Set;
-
-public class AudioAttributesWrapper {
- private static Method getCapturePresetMethod;
- private static Method setCapturePresetMethod;
- private static Method getTagsMethod;
-
- private static Method getGetCapturePresetMethod() throws NoSuchMethodException {
- if (getCapturePresetMethod == null) {
- getCapturePresetMethod = AudioAttributes.class.getMethod("getCapturePreset");
- }
- return getCapturePresetMethod;
- }
-
- public static int getCapturePreset(AudioAttributes audioAttributes)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
- try {
- return (int) getGetCapturePresetMethod().invoke(audioAttributes);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.getCapturePreset()", e);
- throw e;
- }
- }
-
- private static Method getSetCapturePresetMethod() throws NoSuchMethodException {
- if (setCapturePresetMethod == null) {
- setCapturePresetMethod = AudioAttributes.class.getMethod("setCapturePreset", int.class);
- }
- return setCapturePresetMethod;
- }
-
- public static void setCapturePreset(AudioAttributes audioAttributes, int preset)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
- try {
- getSetCapturePresetMethod().invoke(audioAttributes, preset);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.setCapturePreset()", e);
- throw e;
- }
- }
-
- private static Method getGetTagsMethod() throws NoSuchMethodException {
- if (getTagsMethod == null) {
- getTagsMethod = AudioAttributes.class.getMethod("getTags");
- }
- return getTagsMethod;
- }
-
- @SuppressWarnings("unchecked")
- public static Set getTags(AudioAttributes audioAttributes)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException {
- try {
- return (Set) getGetTagsMethod().invoke(audioAttributes);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.getTags()", e);
- throw e;
- }
- }
-
- public static class Builder {
- private static Method setInternalCapturePresetMethod;
- private static Method setPrivacySensitiveMethod;
- private static Method addTagMethod;
-
- private static Method getSetInternalCapturePresetMethod() throws NoSuchMethodException {
- if (setInternalCapturePresetMethod == null) {
- setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset",
- int.class);
- }
- return setInternalCapturePresetMethod;
- }
-
- public static void setInternalCapturePreset(AudioAttributes.Builder builder, int preset)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
- try {
- getSetInternalCapturePresetMethod().invoke(builder, preset);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.Builder.setInternalCapturePreset()", e);
- throw e;
- }
- }
-
- private static Method getSetPrivacySensitiveMethod() throws NoSuchMethodException {
- if (setPrivacySensitiveMethod == null) {
- setPrivacySensitiveMethod = AudioAttributes.Builder.class.getMethod("setPrivacySensitive",
- boolean.class);
- }
- return setPrivacySensitiveMethod;
- }
-
- public static void setPrivacySensitive(AudioAttributes.Builder builder, boolean privacySensitive)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
- try {
- getSetPrivacySensitiveMethod().invoke(builder, privacySensitive);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.Builder.setPrivacySensitive()", e);
- throw e;
- }
- }
-
- private static Method getAddTagMethod() throws NoSuchMethodException {
- if (addTagMethod == null) {
- addTagMethod = AudioAttributes.Builder.class.getMethod("addTag", String.class);
- }
- return addTagMethod;
- }
-
- public static void addTag(AudioAttributes.Builder builder, String tag)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
- try {
- getAddTagMethod().invoke(builder, tag);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioAttributes.Builder.addTag()", e);
- throw e;
- }
- }
- }
-}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
index 71aa5cbe93..f6ee94b7c1 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioFormatWrapper.java
@@ -8,15 +8,10 @@
import android.media.AudioFormat;
public class AudioFormatWrapper {
- public final static int AUDIO_FORMAT_HAS_PROPERTY_NONE = 0x0;
- public final static int AUDIO_FORMAT_HAS_PROPERTY_ENCODING = 0x1 << 0;
- public final static int AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE = 0x1 << 1;
public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK = 0x1 << 2;
public final static int AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK = 0x1 << 3;
private static Method getPropertySetMaskMethod;
- private static Method channelCountFromInChannelMaskMethod;
- private static Method getBytesPerSampleMethod;
private static Method getGetPropertySetMaskMethod() throws NoSuchMethodException {
if (getPropertySetMaskMethod == null) {
@@ -37,42 +32,4 @@ public static int getPropertySetMask(AudioFormat audioFormat)
}
}
- private static Method getChannelCountFromInChannelMaskMethod() throws NoSuchMethodException {
- if (channelCountFromInChannelMaskMethod == null) {
- channelCountFromInChannelMaskMethod = AudioFormat.class.getMethod("channelCountFromInChannelMask",
- int.class);
- }
- return channelCountFromInChannelMaskMethod;
- }
-
- public static int channelCountFromInChannelMask(int channelMask)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
- try {
- return (int) getChannelCountFromInChannelMaskMethod().invoke(null, channelMask);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioFormat.channelCountFromInChannelMask()", e);
- throw e;
- }
- }
-
- private static Method getGetBytesPerSampleMethod() throws NoSuchMethodException {
- if (getBytesPerSampleMethod == null) {
- getBytesPerSampleMethod = AudioFormat.class.getMethod("getBytesPerSample", int.class);
- }
- return getBytesPerSampleMethod;
- }
-
- public static int getBytesPerSample(AudioFormat format, int encoding)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
- try {
- return (int) getGetBytesPerSampleMethod().invoke(format, encoding);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException e) {
- Ln.e("Failed to invoke AudioFormat.getBytesPerSample()", e);
- throw e;
- }
- }
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
index 197eb7929e..6dd8ff9ab5 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/AudioRecordWrapper.java
@@ -15,7 +15,9 @@
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
+import android.media.AudioManager;
import android.media.AudioRecord;
+import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Binder;
import android.os.Build;
@@ -23,11 +25,10 @@
import android.os.Parcel;
public class AudioRecordWrapper {
- public final static String SUBMIX_FIXED_VOLUME = "fixedVolume";
-
private static Method getChannelMaskFromLegacyConfigMethod;
private static Method getCurrentOpPackageNameMethod;
+ @SuppressLint({ "SoonBlockedPrivateApi" })
private static Method getGetChannelMaskFromLegacyConfigMethod() throws NoSuchMethodException {
if (getChannelMaskFromLegacyConfigMethod == null) {
getChannelMaskFromLegacyConfigMethod = AudioRecord.class.getDeclaredMethod("getChannelMaskFromLegacyConfig",
@@ -38,8 +39,7 @@ private static Method getGetChannelMaskFromLegacyConfigMethod() throws NoSuchMet
}
public static int getChannelMaskFromLegacyConfig(int inChannelConfig, boolean allowLegacyConfig)
- throws IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- NoSuchMethodException {
+ throws IllegalArgumentException {
try {
return (int) getGetChannelMaskFromLegacyConfigMethod().invoke(null, inChannelConfig, allowLegacyConfig);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
@@ -58,8 +58,7 @@ private static Method getGetCurrentOpPackageNameMethod() throws NoSuchMethodExce
}
public static String getCurrentOpPackageName(AudioRecord audioRecord)
- throws IllegalAccessException, IllegalArgumentException,
- InvocationTargetException, NoSuchMethodException {
+ throws IllegalArgumentException {
try {
return (String) getGetCurrentOpPackageNameMethod().invoke(audioRecord);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
@@ -70,10 +69,10 @@ public static String getCurrentOpPackageName(AudioRecord audioRecord)
}
@TargetApi(Build.VERSION_CODES.R)
- @SuppressLint({ "WrongConstant", "MissingPermission", "BlockedPrivateApi" })
- public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat format,
- int bufferSizeInBytes, int sessionId, Context context, int maxSharedAudioHistoryMs)
- throws Exception {
+ @SuppressLint({ "WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi" })
+ public static AudioRecord newInstance(AudioFormat format, int bufferSizeInBytes,
+ Context context)
+ throws UnsupportedOperationException {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s
// constructor, requiring `Context`s from real App environment.
//
@@ -104,9 +103,6 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
mRecordingStateField.setAccessible(true);
mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED);
- if (attributes == null) {
- throw new IllegalArgumentException("Illegal null AudioAttributes");
- }
if (format == null) {
throw new IllegalArgumentException("Illegal null AudioFormat");
}
@@ -121,49 +117,30 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
mInitializationLooperField.setAccessible(true);
mInitializationLooperField.set(audioRecord, looper);
- if (AudioAttributesWrapper.getCapturePreset(attributes) == MediaRecorder.AudioSource.REMOTE_SUBMIX) {
- AudioAttributes.Builder audioAttributeBuilder = new AudioAttributes.Builder(attributes);
-
- final Iterator tagsIter = AudioAttributesWrapper.getTags(attributes).iterator();
- while (tagsIter.hasNext()) {
- String tag = tagsIter.next();
- if (tag.equalsIgnoreCase(SUBMIX_FIXED_VOLUME)) {
- // audioRecord.mIsSubmixFullVolume = true;
- Field mIsSubmixFullVolumeField = AudioRecord.class.getDeclaredField("mIsSubmixFullVolume");
- mIsSubmixFullVolumeField.setAccessible(true);
- mIsSubmixFullVolumeField.set(audioRecord, true);
- } else {
- AudioAttributesWrapper.Builder.addTag(audioAttributeBuilder, tag);
- }
- }
-
- AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributeBuilder,
- AudioAttributesWrapper.getCapturePreset(attributes));
- attributes = audioAttributeBuilder.build();
- }
+ // Create `AudioAttributes` with fixed capture preset
+ int audioCapturePreset = MediaRecorder.AudioSource.REMOTE_SUBMIX;
+ AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
+ Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod(
+ "setInternalCapturePreset", int.class);
+ setInternalCapturePresetMethod.invoke(audioAttributesBuilder, audioCapturePreset);
+ AudioAttributes attributes = audioAttributesBuilder.build();
// audioRecord.mAudioAttributes = attributes;
Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes");
mAudioAttributesField.setAccessible(true);
mAudioAttributesField.set(audioRecord, attributes);
+ // Assume `format.getSampleRate()` is always set.
int rate = format.getSampleRate();
- if (rate == AudioFormat.SAMPLE_RATE_UNSPECIFIED) {
- rate = 0;
- }
- int encoding = AudioFormat.ENCODING_DEFAULT;
- if ((AudioFormatWrapper.getPropertySetMask(format)
- & AudioFormatWrapper.AUDIO_FORMAT_HAS_PROPERTY_ENCODING) != 0) {
- encoding = format.getEncoding();
- }
+ // Assume `format.getEncoding()` is always set.
+ int encoding = format.getEncoding();
// audioRecord.audioParamCheck(capturePreset, rate, encoding);
Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class,
int.class);
audioParamCheckMethod.setAccessible(true);
- audioParamCheckMethod.invoke(audioRecord, AudioAttributesWrapper.getCapturePreset(attributes), rate,
- encoding);
+ audioParamCheckMethod.invoke(audioRecord, audioCapturePreset, rate, encoding);
int mChannelIndexMask = 0;
int mChannelMask = 0;
@@ -178,9 +155,12 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
& AudioFormatWrapper.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK) != 0) {
mChannelMask = getChannelMaskFromLegacyConfig(format.getChannelMask(), false);
mChannelCount = format.getChannelCount();
- } else {
+ } else if (mChannelIndexMask == 0) {
mChannelMask = getChannelMaskFromLegacyConfig(AudioFormat.CHANNEL_IN_DEFAULT, false);
- mChannelCount = AudioFormatWrapper.channelCountFromInChannelMask(mChannelMask);
+
+ Method channelCountFromInChannelMaskMethod = AudioFormat.class.getMethod(
+ "channelCountFromInChannelMask", int.class);
+ mChannelCount = (int) channelCountFromInChannelMaskMethod.invoke(null, mChannelMask);
}
Field mChannelIndexMaskField = AudioRecord.class.getDeclaredField("mChannelIndexMask");
@@ -201,7 +181,7 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes);
int[] sampleRate = new int[] { 0 };
- int[] session = new int[] { sessionId };
+ int[] session = new int[] { AudioManager.AUDIO_SESSION_ID_GENERATE };
int initResult;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
@@ -218,22 +198,12 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
attributes, sampleRate, mChannelMask, mChannelIndexMask, audioRecord.getAudioFormat(),
bufferSizeInBytes, session, getCurrentOpPackageName(audioRecord), 0L);
} else {
- AttributionSource attributionSource = context != null ? context.getAttributionSource()
- : AttributionSource.myAttributionSource();
-
- if (attributionSource.getPackageName() == null) {
- // Command line utility
- // attributionSource = attributionSource.withPackageName("uid:" +
- // Binder.getCallingUid());
- Method withPackageNameMethod = AttributionSource.class.getDeclaredMethod("withPackageName",
- String.class);
- withPackageNameMethod.setAccessible(true);
- attributionSource = (AttributionSource) withPackageNameMethod.invoke(attributionSource,
- "uid:" + Binder.getCallingUid());
- }
+ // Assume `context` is never `null`
+ AttributionSource attributionSource = context.getAttributionSource();
+
+ // Assume `attributionSource.getPackageName()` is never null
- // ScopedParcelState attributionSourceState =
- // attributionSource.asScopedParcelState()
+ // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()
Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState");
asScopedParcelStateMethod.setAccessible(true);
@@ -254,13 +224,13 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
initResult = (int) nativeSetupMethod.invoke(audioRecord,
new WeakReference(audioRecord), attributes, sampleRate, mChannelMask,
mChannelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
- attributionSourceParcel, 0L, maxSharedAudioHistoryMs);
+ attributionSourceParcel, 0L, 0);
}
}
if (initResult != AudioRecord.SUCCESS) {
Ln.e("Error code " + initResult + " when initializing native AudioRecord object.");
- return audioRecord;
+ throw new UnsupportedOperationException("Cannot create AudioRecord");
}
// mSampleRate = sampleRate[0]
@@ -281,104 +251,7 @@ public static AudioRecord newInstance(AudioAttributes attributes, AudioFormat fo
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord..", e);
- throw e;
- }
- }
-
- public static final int PRIVACY_SENSITIVE_DEFAULT = -1;
- public static final int PRIVACY_SENSITIVE_DISABLED = 0;
- public static final int PRIVACY_SENSITIVE_ENABLED = 1;
-
- @SuppressLint({ "BlockedPrivateApi" })
- public static AudioRecord build(AudioRecord.Builder builder) {
- try {
- Field mFormatField = AudioRecord.Builder.class.getDeclaredField("mFormat");
- mFormatField.setAccessible(true);
- AudioFormat mFormat = (AudioFormat) mFormatField.get(builder);
-
- if (mFormat == null) {
- mFormat = new AudioFormat.Builder()
- .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
- .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
- .build();
- } else {
- if (mFormat.getEncoding() == AudioFormat.ENCODING_INVALID) {
- mFormat = new AudioFormat.Builder(mFormat)
- .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
- .build();
- }
- if (mFormat.getChannelMask() == AudioFormat.CHANNEL_INVALID
- && mFormat.getChannelIndexMask() == AudioFormat.CHANNEL_INVALID) {
- mFormat = new AudioFormat.Builder(mFormat)
- .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
- .build();
- }
- }
-
- Field mAttributesField = AudioRecord.Builder.class.getDeclaredField("mAttributes");
- mAttributesField.setAccessible(true);
- AudioAttributes mAttributes = (AudioAttributes) mAttributesField.get(builder);
- if (mAttributes == null) {
- AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
- AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributesBuilder,
- MediaRecorder.AudioSource.DEFAULT);
- mAttributes = audioAttributesBuilder.build();
- }
-
- Field mPrivacySensitiveField = AudioRecord.Builder.class.getDeclaredField("mPrivacySensitive");
- mPrivacySensitiveField.setAccessible(true);
- int mPrivacySensitive = (int) mPrivacySensitiveField.get(builder);
- if (mPrivacySensitive != PRIVACY_SENSITIVE_DEFAULT) {
- int source = AudioAttributesWrapper.getCapturePreset(mAttributes);
- if (source == MediaRecorder.AudioSource.REMOTE_SUBMIX
- || source == MediaRecorder.AudioSource.VOICE_DOWNLINK
- || source == MediaRecorder.AudioSource.VOICE_UPLINK
- || source == MediaRecorder.AudioSource.VOICE_CALL) {
- throw new UnsupportedOperationException(
- "Cannot request private capture with source: " + source);
- }
-
- AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
- AudioAttributesWrapper.Builder.setInternalCapturePreset(audioAttributesBuilder, source);
- AudioAttributesWrapper.Builder.setPrivacySensitive(audioAttributesBuilder,
- mPrivacySensitive == PRIVACY_SENSITIVE_ENABLED);
- mAttributes = audioAttributesBuilder.build();
- }
-
- Field mBufferSizeInBytesField = AudioRecord.Builder.class.getDeclaredField("mBufferSizeInBytes");
- mBufferSizeInBytesField.setAccessible(true);
- int mBufferSizeInBytes = (int) mBufferSizeInBytesField.get(builder);
-
- // If the buffer size is not specified,
- // use a single frame for the buffer size and let the
- // native code figure out the minimum buffer size.
- if (mBufferSizeInBytes == 0) {
- mBufferSizeInBytes = mFormat.getChannelCount()
- * AudioFormatWrapper.getBytesPerSample(mFormat, mFormat.getEncoding());
- }
-
- Field mSessionIdField = AudioRecord.Builder.class.getDeclaredField("mSessionId");
- mSessionIdField.setAccessible(true);
- int mSessionId = (int) mSessionIdField.get(builder);
-
- Context mContext;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- Field mContextField = AudioRecord.Builder.class.getDeclaredField("mContext");
- mContextField.setAccessible(true);
- mContext = (Context) mContextField.get(builder);
- } else {
- mContext = null;
- }
-
- final AudioRecord record = newInstance(
- mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext, 0);
- if (record.getState() == AudioRecord.STATE_UNINITIALIZED) {
- // release is not necessary
- throw new UnsupportedOperationException("Cannot create AudioRecord");
- }
- return record;
- } catch (Exception e) {
- throw new UnsupportedOperationException("Cannot create AudioRecord", e);
+ throw new UnsupportedOperationException("Cannot create AudioRecord");
}
}
}