Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c670e061 authored by Eric Laurent's avatar Eric Laurent
Browse files

AudioRecord: Allow to share capture history.

Add the possibility for privileged applications to
share part of their audio capture history with another app.

A privileged app with permission CAPTURE_AUDIO_HOTWORD can share part of
its recent capture history on a given AudioRecord with the following
steps:
1) Specify the maximum time in the past that will be available for other
apps by calling setMaxSharedAudioHistoryMillis() on the AudioRecord.Builder
when creating the AudioRecord
2) Start recording and determine where the other app should start
capturing in the past.
3) Call AudioRecord.shareAudioHistory() with the package name of the app the history
will be shared with and the intended start time for this app's capture relative to
this AudioRecord's start time.
4) Communicate the MediaSyncEvent returned by shareAudioHistory() to the other app
5) The other app will use the MediaSyncEvent when creating its AudioRecord by calling
setSharedAudioEvent() on the AudioRecord.Builder.
6) Only after the other app has started capturing can this app stop capturing and
release its AudioRecord.

Bug: 183705547
Test: regression on capture use cases

Change-Id: I5beba6c1e489148a14ba86165b8ef2fdc78c802a
parent aa36363f
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -23399,11 +23399,14 @@ package android.media {
    method public void onError(@NonNull android.media.MediaSync, int, int);
  }
  public class MediaSyncEvent {
  public class MediaSyncEvent implements android.os.Parcelable {
    method public static android.media.MediaSyncEvent createEvent(int) throws java.lang.IllegalArgumentException;
    method public int describeContents();
    method public int getAudioSessionId();
    method public int getType();
    method public android.media.MediaSyncEvent setAudioSessionId(int) throws java.lang.IllegalArgumentException;
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.media.MediaSyncEvent> CREATOR;
    field public static final int SYNC_EVENT_NONE = 0; // 0x0
    field public static final int SYNC_EVENT_PRESENTATION_COMPLETE = 1; // 0x1
  }
+8 −0
Original line number Diff line number Diff line
@@ -5268,11 +5268,15 @@ package android.media {
  public class AudioRecord implements android.media.AudioRecordingMonitor android.media.AudioRouting android.media.MicrophoneDirection {
    ctor @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public AudioRecord(android.media.AudioAttributes, android.media.AudioFormat, int, int) throws java.lang.IllegalArgumentException;
    method public static long getMaxSharedAudioHistoryMillis();
    method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.MediaSyncEvent shareAudioHistory(@NonNull String, @IntRange(from=0) long);
  }
  public static class AudioRecord.Builder {
    method public android.media.AudioRecord.Builder setAudioAttributes(@NonNull android.media.AudioAttributes) throws java.lang.IllegalArgumentException;
    method @NonNull @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public android.media.AudioRecord.Builder setMaxSharedAudioHistoryMillis(long) throws java.lang.IllegalArgumentException;
    method public android.media.AudioRecord.Builder setSessionId(int) throws java.lang.IllegalArgumentException;
    method @NonNull public android.media.AudioRecord.Builder setSharedAudioEvent(@NonNull android.media.MediaSyncEvent) throws java.lang.IllegalArgumentException;
  }
  public final class AudioRecordingConfiguration implements android.os.Parcelable {
@@ -5336,6 +5340,10 @@ package android.media {
    method public void onPreferredFeaturesChanged(@NonNull java.util.List<java.lang.String>);
  }
  public class MediaSyncEvent implements android.os.Parcelable {
    field public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = 100; // 0x64
  }
  public class PlayerProxy {
    method public void pause();
    method public void setPan(float);
+34 −16
Original line number Diff line number Diff line
@@ -189,7 +189,8 @@ static jint android_media_AudioRecord_setup(JNIEnv *env, jobject thiz, jobject w
                                            jobject jaa, jintArray jSampleRate, jint channelMask,
                                            jint channelIndexMask, jint audioFormat,
                                            jint buffSizeInBytes, jintArray jSession,
                                            jobject jIdentity, jlong nativeRecordInJavaObj) {
                                            jobject jIdentity, jlong nativeRecordInJavaObj,
                                            jint sharedAudioHistoryMs) {
    //ALOGV(">> Entering android_media_AudioRecord_setup");
    //ALOGV("sampleRate=%d, audioFormat=%d, channel mask=%x, buffSizeInBytes=%d "
    //     "nativeRecordInJavaObj=0x%llX",
@@ -288,20 +289,18 @@ static jint android_media_AudioRecord_setup(JNIEnv *env, jobject thiz, jobject w
        lpCallbackData->audioRecord_ref = env->NewGlobalRef(weak_this);
        lpCallbackData->busy = false;

        const status_t status = lpRecorder->set(paa->source,
            sampleRateInHertz,
        const status_t status =
                lpRecorder->set(paa->source, sampleRateInHertz,
                                format, // word length, PCM
            localChanMask,
            frameCount,
                                localChanMask, frameCount,
                                recorderCallback, // callback_t
                                lpCallbackData,   // void* user
                                0,                // notificationFrames,
                                true,             // threadCanCallJava
            sessionId,
            AudioRecord::TRANSFER_DEFAULT,
            flags,
            -1, -1,        // default uid, pid
            paa.get());
                                sessionId, AudioRecord::TRANSFER_DEFAULT, flags, -1,
                                -1, // default uid, pid
                                paa.get(), AUDIO_PORT_HANDLE_NONE, MIC_DIRECTION_UNSPECIFIED,
                                MIC_FIELD_DIMENSION_DEFAULT, sharedAudioHistoryMs);

        if (status != NO_ERROR) {
            ALOGE("Error creating AudioRecord instance: initialization check failed with status %d.",
@@ -877,6 +876,23 @@ static void android_media_AudioRecord_setLogSessionId(JNIEnv *env, jobject thiz,
    record->setLogSessionId(logSessionId.c_str());
}

static jint android_media_AudioRecord_shareAudioHistory(JNIEnv *env, jobject thiz,
                                                        jstring jSharedPackageName,
                                                        jlong jSharedStartMs) {
    sp<AudioRecord> record = getAudioRecord(env, thiz);
    if (record == nullptr) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "Unable to retrieve AudioRecord pointer for setLogSessionId()");
    }
    if (jSharedPackageName == nullptr) {
        jniThrowException(env, "java/lang/IllegalArgumentException", "package name cannot be null");
    }
    ScopedUtfChars nSharedPackageName(env, jSharedPackageName);
    ALOGV("%s: nSharedPackageName '%s'", __func__, nSharedPackageName.c_str());
    return nativeToJavaStatus(record->shareAudioHistory(nSharedPackageName.c_str(),
                                                        static_cast<int64_t>(jSharedStartMs)));
}

// ----------------------------------------------------------------------------
static jint android_media_AudioRecord_get_port_id(JNIEnv *env,  jobject thiz) {
    sp<AudioRecord> lpRecorder = getAudioRecord(env, thiz);
@@ -896,7 +912,7 @@ static const JNINativeMethod gMethods[] = {
        {"native_start", "(II)I", (void *)android_media_AudioRecord_start},
        {"native_stop", "()V", (void *)android_media_AudioRecord_stop},
        {"native_setup",
         "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/media/permission/Identity;J)I",
         "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILandroid/media/permission/Identity;JI)I",
         (void *)android_media_AudioRecord_setup},
        {"native_finalize", "()V", (void *)android_media_AudioRecord_finalize},
        {"native_release", "()V", (void *)android_media_AudioRecord_release},
@@ -936,6 +952,8 @@ static const JNINativeMethod gMethods[] = {
         (void *)android_media_AudioRecord_set_preferred_microphone_field_dimension},
        {"native_setLogSessionId", "(Ljava/lang/String;)V",
         (void *)android_media_AudioRecord_setLogSessionId},
        {"native_shareAudioHistory", "(Ljava/lang/String;J)I",
         (void *)android_media_AudioRecord_shareAudioHistory},
};

// field names found in android/media/AudioRecord.java
+139 −10
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.media.permission.PermissionUtil.myIdentity;
import android.annotation.CallbackExecutor;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -358,7 +359,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
    @RequiresPermission(android.Manifest.permission.RECORD_AUDIO)
    public AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
            int sessionId) throws IllegalArgumentException {
        this(attributes, format, bufferSizeInBytes, sessionId, ActivityThread.currentApplication());
        this(attributes, format, bufferSizeInBytes, sessionId,
                ActivityThread.currentApplication(), 0 /*maxSharedAudioHistoryMs*/);
    }

    /**
@@ -383,7 +385,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
     * @throws IllegalArgumentException
     */
    private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
            int sessionId, @Nullable Context context) throws IllegalArgumentException {
            int sessionId, @Nullable Context context,
            int maxSharedAudioHistoryMs) throws IllegalArgumentException {
        mRecordingState = RECORDSTATE_STOPPED;

        if (attributes == null) {
@@ -455,12 +458,14 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
        int[] sampleRate = new int[] {mSampleRate};
        int[] session = new int[1];
        session[0] = sessionId;

        //TODO: update native initialization when information about hardware init failure
        //      due to capture device already open is available.
        int initResult = native_setup(new WeakReference<AudioRecord>(this),
                mAudioAttributes, sampleRate, mChannelMask, mChannelIndexMask,
                mAudioFormat, mNativeBufferSizeInBytes,
                session, identity, 0 /*nativeRecordInJavaObj*/);
                session, identity, 0 /*nativeRecordInJavaObj*/,
                maxSharedAudioHistoryMs);
        if (initResult != SUCCESS) {
            loge("Error code "+initResult+" when initializing native AudioRecord object.");
            return; // with mState == STATE_UNINITIALIZED
@@ -522,7 +527,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
                    0 /*mNativeBufferSizeInBytes*/,
                    session,
                    myIdentity(null),
                    nativeRecordInJavaObj);
                    nativeRecordInJavaObj,
                    0);
            if (initResult != SUCCESS) {
                loge("Error code "+initResult+" when initializing native AudioRecord object.");
                return; // with mState == STATE_UNINITIALIZED
@@ -581,7 +587,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
        private int mBufferSizeInBytes;
        private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
        private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;

        private int mMaxSharedAudioHistoryMs = 0;
        private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
        private static final int PRIVACY_SENSITIVE_DISABLED = 0;
        private static final int PRIVACY_SENSITIVE_ENABLED = 1;
@@ -747,7 +753,12 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
            if (sessionId < 0) {
                throw new IllegalArgumentException("Invalid session ID " + sessionId);
            }
            // Do not override a session ID previously set with setSharedAudioEvent()
            if (mSessionId == AudioManager.AUDIO_SESSION_ID_GENERATE) {
                mSessionId = sessionId;
            } else {
                Log.e(TAG, "setSessionId() called twice or after setSharedAudioEvent()");
            }
            return this;
        }

@@ -771,6 +782,57 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
            return record;
        }

        /**
         * @hide
         * Specifies the maximum duration in the past of the this AudioRecord's capture buffer
         * that can be shared with another app by calling
         * {@link AudioRecord#shareAudioHistory(String, long)}.
         * @param maxSharedAudioHistoryMillis the maximum duration that will be available
         *                                    in milliseconds.
         * @return the same Builder instance.
         * @throws IllegalArgumentException
         *
         */
        @SystemApi
        @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
        public @NonNull Builder setMaxSharedAudioHistoryMillis(long maxSharedAudioHistoryMillis)
                throws IllegalArgumentException {
            if (maxSharedAudioHistoryMillis <= 0
                    || maxSharedAudioHistoryMillis > MAX_SHARED_AUDIO_HISTORY_MS) {
                throw new IllegalArgumentException("Illegal maxSharedAudioHistoryMillis argument");
            }
            mMaxSharedAudioHistoryMs = (int) maxSharedAudioHistoryMillis;
            return this;
        }

        /**
         * @hide
         * Indicates that this AudioRecord will use the audio history shared by another app's
         * AudioRecord. See {@link AudioRecord#shareAudioHistory(String, long)}.
         * The audio session ID set with {@link AudioRecord.Builder#setSessionId(int)} will be
         * ignored if this method is used.
         * @param event The {@link MediaSyncEvent} provided by the app sharing its audio history
         *              with this AudioRecord.
         * @return the same Builder instance.
         * @throws IllegalArgumentException
         */
        @SystemApi
        public @NonNull Builder setSharedAudioEvent(@NonNull MediaSyncEvent event)
                throws IllegalArgumentException {
            Objects.requireNonNull(event);
            if (event.getType() != MediaSyncEvent.SYNC_EVENT_SHARE_AUDIO_HISTORY) {
                throw new IllegalArgumentException(
                        "Invalid event type " + event.getType());
            }
            if (event.getAudioSessionId() == AudioSystem.AUDIO_SESSION_ALLOCATE) {
                throw new IllegalArgumentException(
                        "Invalid session ID " + event.getAudioSessionId());
            }
            // This prevails over a session ID set with setSessionId()
            mSessionId = event.getAudioSessionId();
            return this;
        }

        /**
         * @return a new {@link AudioRecord} instance successfully initialized with all
         *     the parameters set on this <code>Builder</code>.
@@ -837,7 +899,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
                            * mFormat.getBytesPerSample(mFormat.getEncoding());
                }
                final AudioRecord record = new AudioRecord(
                        mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext);
                        mAttributes, mFormat, mBufferSizeInBytes, mSessionId, mContext,
                                    mMaxSharedAudioHistoryMs);
                if (record.getState() == STATE_UNINITIALIZED) {
                    // release is not necessary
                    throw new UnsupportedOperationException("Cannot create AudioRecord");
@@ -1423,7 +1486,6 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
                || (offsetInShorts + sizeInShorts > audioData.length)) {
            return ERROR_BAD_VALUE;
        }

        return native_read_in_short_array(audioData, offsetInShorts, sizeInShorts,
                readMode == READ_BLOCKING);
    }
@@ -1642,6 +1704,70 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
        return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_INPUTS);
    }

    /**
     * Must match the native definition in frameworks/av/service/audioflinger/Audioflinger.h.
     */
    private static final long MAX_SHARED_AUDIO_HISTORY_MS = 5000;

    /**
     * @hide
     * returns the maximum duration in milliseconds of the audio history that can be requested
     * to be made available to other clients using the same session with
     * {@Link Builder#setMaxSharedAudioHistory(long)}.
     */
    @SystemApi
    public static long getMaxSharedAudioHistoryMillis() {
        return MAX_SHARED_AUDIO_HISTORY_MS;
    }

    /**
     * @hide
     *
     * A privileged app with permission CAPTURE_AUDIO_HOTWORD can share part of its recent
     * capture history on a given AudioRecord with the following steps:
     * 1) Specify the maximum time in the past that will be available for other apps by calling
     * {@link Builder#setMaxSharedAudioHistoryMillis(long)} when creating the AudioRecord.
     * 2) Start recording and determine where the other app should start capturing in the past.
     * 3) Call this method with the package name of the app the history will be shared with and
     * the intended start time for this app's capture relative to this AudioRecord's start time.
     * 4) Communicate the {@link MediaSyncEvent} returned by this method to the other app.
     * 5) The other app will use the MediaSyncEvent when creating its AudioRecord with
     * {@link Builder#setSharedAudioEvent(MediaSyncEvent).
     * 6) Only after the other app has started capturing can this app stop capturing and
     * release its AudioRecord.
     * This method is intended to be called only once: if called multiple times, only the last
     * request will be honored.
     * The implementation is "best effort": if the specified start time if too far in the past
     * compared to the max available history specified, the start time will be adjusted to the
     * start of the available history.
     * @param sharedPackage the package the history will be shared with
     * @param startFromMillis the start time, relative to the initial start time of this
     *        AudioRecord, at which the other AudioRecord will start.
     * @return a {@link MediaSyncEvent} to be communicated to the app this AudioRecord's audio
     *         history will be shared with.
     * @throws IllegalArgumentException
     * @throws SecurityException
     */
    @SystemApi
    @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
    @NonNull public MediaSyncEvent shareAudioHistory(@NonNull String sharedPackage,
                                  @IntRange(from = 0) long startFromMillis) {
        Objects.requireNonNull(sharedPackage);
        if (startFromMillis < 0) {
            throw new IllegalArgumentException("Illegal negative sharedAudioHistoryMs argument");
        }
        int status = native_shareAudioHistory(sharedPackage, startFromMillis);
        if (status == AudioSystem.BAD_VALUE) {
            throw new IllegalArgumentException("Illegal sharedAudioHistoryMs argument");
        } else if (status == AudioSystem.PERMISSION_DENIED) {
            throw new SecurityException("permission CAPTURE_AUDIO_HOTWORD required");
        }
        MediaSyncEvent event =
                MediaSyncEvent.createEvent(MediaSyncEvent.SYNC_EVENT_SHARE_AUDIO_HISTORY);
        event.setAudioSessionId(mSessionId);
        return event;
    }

    /*
     * Call BEFORE adding a routing callback handler.
     */
@@ -2105,13 +2231,14 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
        identity.packageName = opPackageName;

        return native_setup(audiorecordThis, attributes, sampleRate, channelMask, channelIndexMask,
                audioFormat, buffSizeInBytes, sessionId, identity, nativeRecordInJavaObj);
                audioFormat, buffSizeInBytes, sessionId, identity, nativeRecordInJavaObj, 0);
    }

    private native int native_setup(Object audiorecordThis,
            Object /*AudioAttributes*/ attributes,
            int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
            int buffSizeInBytes, int[] sessionId, Identity identity, long nativeRecordInJavaObj);
            int buffSizeInBytes, int[] sessionId, Identity identity, long nativeRecordInJavaObj,
            int maxSharedAudioHistoryMs);

    // TODO remove: implementation calls directly into implementation of native_release()
    private native void native_finalize();
@@ -2170,6 +2297,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,

    private native void native_setLogSessionId(@Nullable String logSessionId);

    private native int native_shareAudioHistory(@NonNull String sharedPackage, long startFromMs);

    //---------------------------------------------------------
    // Utility methods
    //------------------
+4 −0
Original line number Diff line number Diff line
@@ -1464,6 +1464,10 @@ public class AudioSystem
    // usage for AudioRecord.startRecordingSync(), must match AudioSystem::sync_event_t
    /** @hide */ public static final int SYNC_EVENT_NONE = 0;
    /** @hide */ public static final int SYNC_EVENT_PRESENTATION_COMPLETE = 1;
    /** @hide
     *  Not used by native implementation.
     *  See {@link AudioRecord.Builder#setSharedAudioEvent(MediaSyncEvent) */
    public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = 100;

    /**
     * @hide
Loading