From c670e06157e2f741f0a03b071fb4e688e0b9ff90 Mon Sep 17 00:00:00 2001 From: Eric Laurent Date: Thu, 8 Apr 2021 20:41:47 +0200 Subject: [PATCH] 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 --- core/api/current.txt | 5 +- core/api/system-current.txt | 8 + core/jni/android_media_AudioRecord.cpp | 50 +++++-- media/java/android/media/AudioRecord.java | 149 +++++++++++++++++-- media/java/android/media/AudioSystem.java | 4 + media/java/android/media/MediaSyncEvent.java | 104 ++++++++++++- 6 files changed, 286 insertions(+), 34 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 41acab61e0818..d20ce61af3f42 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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 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 e77334358efd4..54dc1cf480571 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -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); } + 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; + } + } + }