diff --git a/core/api/current.txt b/core/api/current.txt index 4f28266475a5c..6d59da1230647 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -23413,11 +23413,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 CREATOR; field public static final int SYNC_EVENT_NONE = 0; // 0x0 field public static final int SYNC_EVENT_PRESENTATION_COMPLETE = 1; // 0x1 } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2b56f1d2be44f..648201f06e32a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5271,11 +5271,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 { @@ -5338,6 +5342,10 @@ package android.media { method public void onPreferredFeaturesChanged(@NonNull java.util.List); } + 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); diff --git a/core/jni/android_media_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp index 7d3febbd5d144..83dc1e0bb0d99 100644 --- a/core/jni/android_media_AudioRecord.cpp +++ b/core/jni/android_media_AudioRecord.cpp @@ -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, - format, // word length, PCM - localChanMask, - frameCount, - recorderCallback,// callback_t - lpCallbackData,// void* user - 0, // notificationFrames, - true, // threadCanCallJava - sessionId, - AudioRecord::TRANSFER_DEFAULT, - flags, - -1, -1, // default uid, pid - paa.get()); + const status_t status = + lpRecorder->set(paa->source, sampleRateInHertz, + format, // word length, PCM + localChanMask, frameCount, + recorderCallback, // callback_t + lpCallbackData, // void* user + 0, // notificationFrames, + true, // threadCanCallJava + 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 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(jSharedStartMs))); +} + // ---------------------------------------------------------------------------- static jint android_media_AudioRecord_get_port_id(JNIEnv *env, jobject thiz) { sp 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 diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 3399377be999d..0d44a8580ef98 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -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(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); } - mSessionId = 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 Builder. @@ -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 //------------------ diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 363da248eb96a..3952e18439165 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -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 diff --git a/media/java/android/media/MediaSyncEvent.java b/media/java/android/media/MediaSyncEvent.java index 04448f04ef5cb..fa7d5b897995c 100644 --- a/media/java/android/media/MediaSyncEvent.java +++ b/media/java/android/media/MediaSyncEvent.java @@ -16,6 +16,13 @@ package android.media; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + /** * The MediaSyncEvent class defines events that can be used to synchronize playback or capture * actions between different players and recorders. @@ -24,7 +31,7 @@ package android.media; * The audio session ID is retrieved from a player (e.g {@link MediaPlayer}, {@link AudioTrack} or * {@link ToneGenerator}) by use of the getAudioSessionId() method. */ -public class MediaSyncEvent { +public class MediaSyncEvent implements Parcelable { /** * No sync event specified. When used with a synchronized playback or capture method, the @@ -39,8 +46,16 @@ public class MediaSyncEvent { * {@link #setAudioSessionId(int)} method. */ public static final int SYNC_EVENT_PRESENTATION_COMPLETE = - AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE; + AudioSystem.SYNC_EVENT_PRESENTATION_COMPLETE; + /** + * @hide + * Used when sharing audio history between AudioRecord instances. + * See {@link AudioRecord.Builder#setSharedAudioEvent(MediaSyncEvent). + */ + @SystemApi + public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = + AudioSystem.SYNC_EVENT_SHARE_AUDIO_HISTORY; /** * Creates a synchronization event of the sepcified type. @@ -112,11 +127,86 @@ public class MediaSyncEvent { private static boolean isValidType(int type) { switch (type) { - case SYNC_EVENT_NONE: - case SYNC_EVENT_PRESENTATION_COMPLETE: - return true; - default: - return false; + case SYNC_EVENT_NONE: + case SYNC_EVENT_PRESENTATION_COMPLETE: + case SYNC_EVENT_SHARE_AUDIO_HISTORY: + return true; + default: + return false; } } + + // Parcelable implementation + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + Objects.requireNonNull(dest); + dest.writeInt(mType); + dest.writeInt(mAudioSession); + } + + private MediaSyncEvent(Parcel in) { + mType = in.readInt(); + mAudioSession = in.readInt(); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + /** + * Rebuilds an MediaSyncEvent previously stored with writeToParcel(). + * @param p Parcel object to read the MediaSyncEvent from + * @return a new MediaSyncEvent created from the data in the parcel + */ + public MediaSyncEvent createFromParcel(Parcel p) { + return new MediaSyncEvent(p); + } + public MediaSyncEvent[] newArray(int size) { + return new MediaSyncEvent[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MediaSyncEvent that = (MediaSyncEvent) o; + return ((mType == that.mType) + && (mAudioSession == that.mAudioSession)); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mAudioSession); + } + + @Override + public String toString() { + return new String("MediaSyncEvent:" + + " type=" + typeToString(mType) + + " session=" + mAudioSession); + } + + /** + * Returns the string representation for the type. + * @param type one of the {@link MediaSyncEvent} type constants + * @hide + */ + public static @NonNull String typeToString(int type) { + switch (type) { + case SYNC_EVENT_NONE: + return "SYNC_EVENT_NONE"; + case SYNC_EVENT_PRESENTATION_COMPLETE: + return "SYNC_EVENT_PRESENTATION_COMPLETE"; + case SYNC_EVENT_SHARE_AUDIO_HISTORY: + return "SYNC_EVENT_SHARE_AUDIO_HISTORY"; + default: + return "unknown event type " + type; + } + } + }