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"); } } }