diff --git a/api/system-current.txt b/api/system-current.txt index dc418989d3ebd..ea64dc2b73018 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3456,15 +3456,18 @@ package android.media { method public void clearAudioServerStateCallback(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int dispatchAudioFocusChange(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.audiopolicy.AudioProductStrategies getAudioProductStrategies(); + method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.audiopolicy.AudioVolumeGroups getAudioVolumeGroups(); method public boolean isAudioServerRunning(); method public boolean isHdmiSystemAudioSupported(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); + method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, @NonNull android.media.AudioAttributes, int, int) throws java.lang.IllegalArgumentException; method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.MODIFY_AUDIO_ROUTING}) public int requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, @NonNull android.media.AudioAttributes, int, int, android.media.audiopolicy.AudioPolicy) throws java.lang.IllegalArgumentException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int requestAudioFocus(@NonNull android.media.AudioFocusRequest, @Nullable android.media.audiopolicy.AudioPolicy); method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy); + method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1 field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4 field public static final int AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS = 2; // 0x2 @@ -3478,6 +3481,11 @@ package android.media { method public void onAudioServerUp(); } + public abstract static class AudioManager.VolumeGroupCallback { + ctor public AudioManager.VolumeGroupCallback(); + method public void onAudioVolumeGroupChanged(int, int); + } + public final class AudioPlaybackConfiguration implements android.os.Parcelable { method public int getClientPid(); method public int getClientUid(); @@ -3649,6 +3657,8 @@ package android.media.audiopolicy { method @Nullable public android.media.audiopolicy.AudioProductStrategy getById(int); method public int getLegacyStreamTypeForAudioAttributes(@NonNull android.media.AudioAttributes); method @Nullable public android.media.audiopolicy.AudioProductStrategy getProductStrategyForAudioAttributes(@NonNull android.media.AudioAttributes); + method public int getVolumeGroupIdForAttributes(@NonNull android.media.AudioAttributes); + method public int getVolumeGroupIdForLegacyStreamType(int); method public java.util.Iterator iterator(); method public int size(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -3664,6 +3674,27 @@ package android.media.audiopolicy { field public static final android.os.Parcelable.Creator CREATOR; } + public final class AudioVolumeGroup implements android.os.Parcelable { + method public int describeContents(); + method public java.util.List getAudioAttributes(); + method public int getId(); + method @NonNull public int[] getLegacyStreamTypes(); + method @NonNull public String name(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public final class AudioVolumeGroups implements java.lang.Iterable android.os.Parcelable { + ctor public AudioVolumeGroups(); + method public int describeContents(); + method @Nullable public android.media.audiopolicy.AudioVolumeGroup getById(int); + method public java.util.Iterator iterator(); + method public int size(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int DEFAULT_VOLUME_GROUP = -1; // 0xffffffff + } + } package android.media.session { diff --git a/core/jni/Android.bp b/core/jni/Android.bp index c309f276d3eda..cd34d2e1b441d 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -165,6 +165,8 @@ cc_library_shared { "android_media_AudioTrack.cpp", "android_media_AudioAttributes.cpp", "android_media_AudioProductStrategies.cpp", + "android_media_AudioVolumeGroups.cpp", + "android_media_AudioVolumeGroupCallback.cpp", "android_media_DeviceCallback.cpp", "android_media_JetPlayer.cpp", "android_media_MediaMetricsJNI.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 16517bf87afe4..da27852a3a350 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -110,6 +110,8 @@ extern int register_android_media_AudioSystem(JNIEnv *env); extern int register_android_media_AudioTrack(JNIEnv *env); extern int register_android_media_AudioAttributes(JNIEnv *env); extern int register_android_media_AudioProductStrategies(JNIEnv *env); +extern int register_android_media_AudioVolumeGroups(JNIEnv *env); +extern int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env); extern int register_android_media_MicrophoneInfo(JNIEnv *env); extern int register_android_media_JetPlayer(JNIEnv *env); extern int register_android_media_ToneGenerator(JNIEnv *env); @@ -1524,6 +1526,8 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_media_AudioTrack), REG_JNI(register_android_media_AudioAttributes), REG_JNI(register_android_media_AudioProductStrategies), + REG_JNI(register_android_media_AudioVolumeGroups), + REG_JNI(register_android_media_AudioVolumeGroupChangeHandler), REG_JNI(register_android_media_JetPlayer), REG_JNI(register_android_media_MicrophoneInfo), REG_JNI(register_android_media_RemoteDisplay), diff --git a/core/jni/android_media_AudioProductStrategies.cpp b/core/jni/android_media_AudioProductStrategies.cpp index d7d31e59ad30f..822b74a6b9901 100644 --- a/core/jni/android_media_AudioProductStrategies.cpp +++ b/core/jni/android_media_AudioProductStrategies.cpp @@ -57,7 +57,7 @@ static struct { static jclass gAudioAttributesGroupClass; static jmethodID gAudioAttributesGroupCstor; static struct { - jfieldID mGroupId; + jfieldID mVolumeGroupId; jfieldID mLegacyStreamType; jfieldID mAudioAttributes; } gAudioAttributesGroupsFields; @@ -207,7 +207,7 @@ android_media_AudioSystem_getProductStrategyFromAudioAttributes(JNIEnv *env, job } product_strategy_t psId; status_t status = AudioSystem::getProductStrategyFromAudioAttributes( - AudioAttributes(*attributes.get()), psId); + AudioAttributes(*attributes.get()), psId); if (status != NO_ERROR) { return nativeToJavaStatus(status); } @@ -249,8 +249,8 @@ int register_android_media_AudioProductStrategies(JNIEnv *env) gAudioAttributesGroupClass = MakeGlobalRefOrDie(env, audioAttributesGroupClass); gAudioAttributesGroupCstor = GetMethodIDOrDie(env, audioAttributesGroupClass, "", "(II[Landroid/media/AudioAttributes;)V"); - gAudioAttributesGroupsFields.mGroupId = GetFieldIDOrDie( - env, audioAttributesGroupClass, "mGroupId", "I"); + gAudioAttributesGroupsFields.mVolumeGroupId = GetFieldIDOrDie( + env, audioAttributesGroupClass, "mVolumeGroupId", "I"); gAudioAttributesGroupsFields.mLegacyStreamType = GetFieldIDOrDie( env, audioAttributesGroupClass, "mLegacyStreamType", "I"); gAudioAttributesGroupsFields.mAudioAttributes = GetFieldIDOrDie( diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp new file mode 100644 index 0000000000000..cb4ddbd119d58 --- /dev/null +++ b/core/jni/android_media_AudioVolumeGroupCallback.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 + +#define LOG_TAG "AudioVolumeGroupCallback-JNI" + +#include +#include +#include "core_jni_helpers.h" + +#include "android_media_AudioVolumeGroupCallback.h" + + +// ---------------------------------------------------------------------------- +using namespace android; + +static const char* const kAudioVolumeGroupChangeHandlerClassPathName = + "android/media/audiopolicy/AudioVolumeGroupChangeHandler"; + +static struct { + jfieldID mJniCallback; +} gAudioVolumeGroupChangeHandlerFields; + +static struct { + jmethodID postEventFromNative; +} gAudioVolumeGroupChangeHandlerMethods; + +static Mutex gLock; + +JNIAudioVolumeGroupCallback::JNIAudioVolumeGroupCallback(JNIEnv* env, + jobject thiz, + jobject weak_thiz) +{ + jclass clazz = env->GetObjectClass(thiz); + if (clazz == NULL) { + ALOGE("Can't find class %s", kAudioVolumeGroupChangeHandlerClassPathName); + return; + } + mClass = (jclass)env->NewGlobalRef(clazz); + + // We use a weak reference so the AudioVolumeGroupChangeHandler object can be garbage collected. + // The reference is only used as a proxy for callbacks. + mObject = env->NewGlobalRef(weak_thiz); +} + +JNIAudioVolumeGroupCallback::~JNIAudioVolumeGroupCallback() +{ + // remove global references + JNIEnv *env = AndroidRuntime::getJNIEnv(); + if (env == NULL) { + return; + } + env->DeleteGlobalRef(mObject); + env->DeleteGlobalRef(mClass); +} + +void JNIAudioVolumeGroupCallback::onAudioVolumeGroupChanged(volume_group_t group, int flags) +{ + JNIEnv *env = AndroidRuntime::getJNIEnv(); + if (env == NULL) { + return; + } + ALOGV("%s volume group id %d", __FUNCTION__, group); + env->CallStaticVoidMethod(mClass, + gAudioVolumeGroupChangeHandlerMethods.postEventFromNative, + mObject, + AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED, group, flags, NULL); + if (env->ExceptionCheck()) { + ALOGW("An exception occurred while notifying an event."); + env->ExceptionClear(); + } +} + +void JNIAudioVolumeGroupCallback::onServiceDied() +{ + JNIEnv *env = AndroidRuntime::getJNIEnv(); + if (env == NULL) { + return; + } + env->CallStaticVoidMethod(mClass, + gAudioVolumeGroupChangeHandlerMethods.postEventFromNative, + mObject, + AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED, 0, 0, NULL); + if (env->ExceptionCheck()) { + ALOGW("An exception occurred while notifying an event."); + env->ExceptionClear(); + } +} + +static +sp setJniCallback(JNIEnv* env, + jobject thiz, + const sp& callback) +{ + Mutex::Autolock l(gLock); + sp old = (JNIAudioVolumeGroupCallback*)env->GetLongField( + thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback); + if (callback.get()) { + callback->incStrong((void*)setJniCallback); + } + if (old != 0) { + old->decStrong((void*)setJniCallback); + } + env->SetLongField(thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback, + (jlong)callback.get()); + return old; +} + +static void +android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup(JNIEnv *env, + jobject thiz, + jobject weak_this) +{ + ALOGV("%s", __FUNCTION__); + sp callback = + new JNIAudioVolumeGroupCallback(env, thiz, weak_this); + + if (AudioSystem::addAudioVolumeGroupCallback(callback) == NO_ERROR) { + setJniCallback(env, thiz, callback); + } +} + +static void +android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize(JNIEnv *env, jobject thiz) +{ + ALOGV("%s", __FUNCTION__); + sp callback = setJniCallback(env, thiz, 0); + if (callback != 0) { + AudioSystem::removeAudioVolumeGroupCallback(callback); + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + {"native_setup", "(Ljava/lang/Object;)V", + (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup}, + {"native_finalize", "()V", + (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize}, +}; + +int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env) +{ + jclass audioVolumeGroupChangeHandlerClass = + FindClassOrDie(env, kAudioVolumeGroupChangeHandlerClassPathName); + gAudioVolumeGroupChangeHandlerMethods.postEventFromNative = + GetStaticMethodIDOrDie(env, audioVolumeGroupChangeHandlerClass, "postEventFromNative", + "(Ljava/lang/Object;IIILjava/lang/Object;)V"); + + gAudioVolumeGroupChangeHandlerFields.mJniCallback = + GetFieldIDOrDie(env, audioVolumeGroupChangeHandlerClass, "mJniCallback", "J"); + + env->DeleteLocalRef(audioVolumeGroupChangeHandlerClass); + + return RegisterMethodsOrDie(env, + kAudioVolumeGroupChangeHandlerClassPathName, + gMethods, + NELEM(gMethods)); +} + diff --git a/core/jni/android_media_AudioVolumeGroupCallback.h b/core/jni/android_media_AudioVolumeGroupCallback.h new file mode 100644 index 0000000000000..de06549621b9f --- /dev/null +++ b/core/jni/android_media_AudioVolumeGroupCallback.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android { + +// keep in sync with AudioManager.AudioVolumeGroupChangeHandler.java +#define AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED 1000 +#define AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED 1001 + +class JNIAudioVolumeGroupCallback: public AudioSystem::AudioVolumeGroupCallback +{ +public: + JNIAudioVolumeGroupCallback(JNIEnv* env, jobject thiz, jobject weak_thiz); + ~JNIAudioVolumeGroupCallback(); + + void onAudioVolumeGroupChanged(volume_group_t group, int flags) override; + void onServiceDied() override; + +private: + void sendEvent(int event); + + jclass mClass; /**< Reference to AudioVolumeGroupChangeHandler class. */ + jobject mObject; /**< Weak ref to AudioVolumeGroupChangeHandler object to call on. */ +}; + +} // namespace android diff --git a/core/jni/android_media_AudioVolumeGroups.cpp b/core/jni/android_media_AudioVolumeGroups.cpp new file mode 100644 index 0000000000000..64f0c1e33e1c3 --- /dev/null +++ b/core/jni/android_media_AudioVolumeGroups.cpp @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#define LOG_NDEBUG 0 + +#define LOG_TAG "AudioVolumeGroups-JNI" + +#include +#include +#include +#include "core_jni_helpers.h" + +#include +#include + +#include +#include + +#include + +#include "android_media_AudioAttributes.h" +#include "android_media_AudioErrors.h" + +// ---------------------------------------------------------------------------- + +using namespace android; + +// ---------------------------------------------------------------------------- +static const char* const kClassPathName = "android/media/audiopolicy/AudioVolumeGroups"; +static const char* const kAudioVolumeGroupClassPathName = + "android/media/audiopolicy/AudioVolumeGroup"; + +static jclass gAudioVolumeGroupClass; +static jmethodID gAudioVolumeGroupCstor; +static struct { + jfieldID mName; + jfieldID mId; +} gAudioVolumeGroupFields; + +static jclass gArrayListClass; +static jmethodID gArrayListCstor; +static struct { + jmethodID add; + jmethodID toArray; +} gArrayListMethods; + + +static jint convertAudioVolumeGroupsFromNative( + JNIEnv *env, jobject *jGroup, const AudioVolumeGroup &group) +{ + jint jStatus = (jint)AUDIO_JAVA_SUCCESS; + jstring jName = NULL; + jint Id = NULL; + + jintArray jLegacyStreamTypes = NULL; + jobjectArray jAudioAttributes = NULL; + jint numAttributes; + jobject jAudioAttribute = NULL; + + jName = env->NewStringUTF(group.getName().c_str()); + Id = static_cast(group.getId()); + + // Legacy stream types array + jLegacyStreamTypes = env->NewIntArray(group.getStreamTypes().size()); + if (jLegacyStreamTypes == NULL) { + jStatus = (jint)AUDIO_JAVA_ERROR; + goto exit; + } + for (size_t streamIndex = 0; streamIndex < group.getStreamTypes().size(); streamIndex++) { + jint jStream = group.getStreamTypes()[streamIndex]; + env->SetIntArrayRegion(jLegacyStreamTypes, streamIndex, 1, &jStream); + } + + // Audio Attributes array + numAttributes = group.getAudioAttributes().size(); + jStatus = JNIAudioAttributeHelper::getJavaArray(env, &jAudioAttributes, numAttributes); + if (jStatus != (jint)AUDIO_JAVA_SUCCESS) { + goto exit; + } + + for (size_t j = 0; j < static_cast(numAttributes); j++) { + auto attributes = group.getAudioAttributes()[j]; + + jStatus = JNIAudioAttributeHelper::nativeToJava(env, &jAudioAttribute, attributes); + if (jStatus != AUDIO_JAVA_SUCCESS) { + goto exit; + } + env->SetObjectArrayElement(jAudioAttributes, j, jAudioAttribute); + } + + *jGroup = env->NewObject(gAudioVolumeGroupClass, gAudioVolumeGroupCstor, + jName, Id, jAudioAttributes, jLegacyStreamTypes); +exit: + if (jName != NULL) { + env->DeleteLocalRef(jName); + } + return jStatus; +} + +static jint +android_media_AudioSystem_listAudioVolumeGroups(JNIEnv *env, jobject clazz, jobject jVolumeGroups) +{ + if (env == NULL) { + return AUDIO_JAVA_DEAD_OBJECT; + } + if (jVolumeGroups == NULL) { + ALOGE("listAudioVolumeGroups NULL AudioVolumeGroups"); + return (jint)AUDIO_JAVA_BAD_VALUE; + } + if (!env->IsInstanceOf(jVolumeGroups, gArrayListClass)) { + ALOGE("listAudioVolumeGroups not an arraylist"); + return (jint)AUDIO_JAVA_BAD_VALUE; + } + + status_t status; + AudioVolumeGroupVector groups; + jint jStatus; + jobject jGroup = NULL; + + status = AudioSystem::listAudioVolumeGroups(groups); + if (status != NO_ERROR) { + ALOGE("AudioSystem::listAudioVolumeGroups error %d", status); + return nativeToJavaStatus(status); + } + for (const auto &group : groups) { + jStatus = convertAudioVolumeGroupsFromNative(env, &jGroup, group); + if (jStatus != AUDIO_JAVA_SUCCESS) { + goto exit; + } + env->CallBooleanMethod(jVolumeGroups, gArrayListMethods.add, jGroup); + } +exit: + if (jGroup != NULL) { + env->DeleteLocalRef(jGroup); + } + return jStatus; +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + {"native_list_audio_volume_groups", "(Ljava/util/ArrayList;)I", + (void *)android_media_AudioSystem_listAudioVolumeGroups}, +}; + +int register_android_media_AudioVolumeGroups(JNIEnv *env) +{ + jclass arrayListClass = FindClassOrDie(env, "java/util/ArrayList"); + gArrayListClass = MakeGlobalRefOrDie(env, arrayListClass); + gArrayListCstor = GetMethodIDOrDie(env, arrayListClass, "", "()V"); + gArrayListMethods.add = GetMethodIDOrDie(env, arrayListClass, "add", "(Ljava/lang/Object;)Z"); + gArrayListMethods.toArray = GetMethodIDOrDie(env, arrayListClass, + "toArray", "()[Ljava/lang/Object;"); + + jclass audioVolumeGroupClass = FindClassOrDie(env, kAudioVolumeGroupClassPathName); + gAudioVolumeGroupClass = MakeGlobalRefOrDie(env, audioVolumeGroupClass); + gAudioVolumeGroupCstor = GetMethodIDOrDie( + env, audioVolumeGroupClass, "", + "(Ljava/lang/String;I[Landroid/media/AudioAttributes;[I)V"); + + gAudioVolumeGroupFields.mName = GetFieldIDOrDie( + env, audioVolumeGroupClass, "mName", "Ljava/lang/String;"); + gAudioVolumeGroupFields.mId = GetFieldIDOrDie( + env, audioVolumeGroupClass, "mId", "I"); + + env->DeleteLocalRef(audioVolumeGroupClass); + + return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); +} diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 3fb2365cb0065..69008ede0d7ea 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -37,6 +37,8 @@ import android.content.Intent; import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; import android.media.audiopolicy.AudioProductStrategies; +import android.media.audiopolicy.AudioVolumeGroupChangeHandler; +import android.media.audiopolicy.AudioVolumeGroups; import android.media.projection.MediaProjection; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -61,6 +63,7 @@ import android.util.Pair; import android.view.KeyEvent; import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; import java.io.IOException; import java.lang.annotation.Retention; @@ -90,6 +93,8 @@ public class AudioManager { private static final String TAG = "AudioManager"; private static final boolean DEBUG = false; private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler(); + private static final AudioVolumeGroupChangeHandler sAudioAudioVolumeGroupChangedHandler = + new AudioVolumeGroupChangeHandler(); /** * Broadcast intent, a hint for applications that audio is about to become @@ -5246,6 +5251,60 @@ public class AudioManager { } } + /** + * @hide + * Introspection API to retrieve audio volume groups. + * When implementing {Car|Oem}AudioManager, use this method to retrieve the collection of + * audio volume groups. + * @return a (possibly zero-length) array of + * {@see android.media.audiopolicy.AudioVolumeGroups} objects. + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public @NonNull AudioVolumeGroups getAudioVolumeGroups() { + return new AudioVolumeGroups(); + } + + /** + * @hide + * Callback registered by client to be notified upon volume group change. + */ + @SystemApi + public abstract static class VolumeGroupCallback { + /** + * Callback method called upon audio volume group change. + * @param group the group for which the volume has changed + */ + public void onAudioVolumeGroupChanged(int group, int flags) {} + } + + /** + * @hide + * Register an audio volume group change listener. + * @param callback the {@link VolumeGroupCallback} to register + */ + @SystemApi + public void registerVolumeGroupCallback( + @NonNull Executor executor, + @NonNull VolumeGroupCallback callback) { + Preconditions.checkNotNull(executor, "executor must not be null"); + Preconditions.checkNotNull(callback, "volume group change cb must not be null"); + sAudioAudioVolumeGroupChangedHandler.init(); + // TODO: make use of executor + sAudioAudioVolumeGroupChangedHandler.registerListener(callback); + } + + /** + * @hide + * Unregister an audio volume group change listener. + * @param callback the {@link VolumeGroupCallback} to unregister + */ + @SystemApi + public void unregisterVolumeGroupCallback( + @NonNull VolumeGroupCallback callback) { + Preconditions.checkNotNull(callback, "volume group change cb must not be null"); + sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback); + } //--------------------------------------------------------- // Inner classes diff --git a/media/java/android/media/audiopolicy/AudioProductStrategies.java b/media/java/android/media/audiopolicy/AudioProductStrategies.java index 6a2375fe153d4..d5938853569ca 100644 --- a/media/java/android/media/audiopolicy/AudioProductStrategies.java +++ b/media/java/android/media/audiopolicy/AudioProductStrategies.java @@ -177,7 +177,50 @@ public final class AudioProductStrategies implements Iterable CREATOR = new Parcelable.Creator() { @Override diff --git a/media/java/android/media/audiopolicy/AudioProductStrategy.java b/media/java/android/media/audiopolicy/AudioProductStrategy.java index af6e8bfdbd83f..169484b79bd4c 100644 --- a/media/java/android/media/audiopolicy/AudioProductStrategy.java +++ b/media/java/android/media/audiopolicy/AudioProductStrategy.java @@ -157,32 +157,32 @@ public final class AudioProductStrategy implements Parcelable { /** * @hide * @param streamType legacy stream type used for volume operation only - * @return the {@link AudioAttributes} relevant for the given streamType. - * If none is found, it builds the default attributes. + * @return the volume group id relevant for the given streamType. + * If none is found, {@link AudioVolumeGroups#DEFAULT_VOLUME_GROUP} is returned. */ - public int getGroupIdForLegacyStreamType(int streamType) { + public int getVolumeGroupIdForLegacyStreamType(int streamType) { for (final AudioAttributesGroup aag : mAudioAttributesGroups) { if (aag.supportsStreamType(streamType)) { - return aag.getGroupId(); + return aag.getVolumeGroupId(); } } - return DEFAULT_GROUP; + return AudioVolumeGroups.DEFAULT_VOLUME_GROUP; } /** * @hide * @param aa the {@link AudioAttributes} to be considered - * @return the group id associated with the given audio attributes if found, - * default value otherwise. + * @return the volume group id associated with the given audio attributes if found, + * {@link AudioVolumeGroups#DEFAULT_VOLUME_GROUP} otherwise. */ - public int getGroupIdForAudioAttributes(@NonNull AudioAttributes aa) { + public int getVolumeGroupIdForAudioAttributes(@NonNull AudioAttributes aa) { Preconditions.checkNotNull(aa, "AudioAttributes must not be null"); for (final AudioAttributesGroup aag : mAudioAttributesGroups) { if (aag.supportsAttributes(aa)) { - return aag.getGroupId(); + return aag.getVolumeGroupId(); } } - return DEFAULT_GROUP; + return AudioVolumeGroups.DEFAULT_VOLUME_GROUP; } @Override @@ -266,15 +266,14 @@ public final class AudioProductStrategy implements Parcelable { && ((refFormattedTags.length() == 0) || refFormattedTags.equals(cliFormattedTags)); } - private static final class AudioAttributesGroup implements Parcelable { - private int mGroupId; + private int mVolumeGroupId; private int mLegacyStreamType; private final AudioAttributes[] mAudioAttributes; - AudioAttributesGroup(int groupId, int streamType, + AudioAttributesGroup(int volumeGroupId, int streamType, @NonNull AudioAttributes[] audioAttributes) { - mGroupId = groupId; + mVolumeGroupId = volumeGroupId; mLegacyStreamType = streamType; mAudioAttributes = audioAttributes; } @@ -286,7 +285,7 @@ public final class AudioProductStrategy implements Parcelable { AudioAttributesGroup thatAag = (AudioAttributesGroup) o; - return mGroupId == thatAag.mGroupId + return mVolumeGroupId == thatAag.mVolumeGroupId && mLegacyStreamType == thatAag.mLegacyStreamType && mAudioAttributes.equals(thatAag.mAudioAttributes); } @@ -295,8 +294,8 @@ public final class AudioProductStrategy implements Parcelable { return mLegacyStreamType; } - public int getGroupId() { - return mGroupId; + public int getVolumeGroupId() { + return mVolumeGroupId; } public @NonNull AudioAttributes getAudioAttributes() { @@ -331,7 +330,7 @@ public final class AudioProductStrategy implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeInt(mGroupId); + dest.writeInt(mVolumeGroupId); dest.writeInt(mLegacyStreamType); dest.writeInt(mAudioAttributes.length); for (AudioAttributes attributes : mAudioAttributes) { @@ -343,14 +342,14 @@ public final class AudioProductStrategy implements Parcelable { new Parcelable.Creator() { @Override public AudioAttributesGroup createFromParcel(@NonNull Parcel in) { - int groupId = in.readInt(); + int volumeGroupId = in.readInt(); int streamType = in.readInt(); int nbAttributes = in.readInt(); AudioAttributes[] aa = new AudioAttributes[nbAttributes]; for (int index = 0; index < nbAttributes; index++) { aa[index] = AudioAttributes.CREATOR.createFromParcel(in); } - return new AudioAttributesGroup(groupId, streamType, aa); + return new AudioAttributesGroup(volumeGroupId, streamType, aa); } @Override @@ -365,8 +364,8 @@ public final class AudioProductStrategy implements Parcelable { StringBuilder s = new StringBuilder(); s.append("\n Legacy Stream Type: "); s.append(Integer.toString(mLegacyStreamType)); - s.append(" Group Id: "); - s.append(Integer.toString(mGroupId)); + s.append(" Volume Group Id: "); + s.append(Integer.toString(mVolumeGroupId)); for (AudioAttributes attribute : mAudioAttributes) { s.append("\n -"); diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroup.aidl b/media/java/android/media/audiopolicy/AudioVolumeGroup.aidl new file mode 100644 index 0000000000000..caf1e0db1ad77 --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioVolumeGroup.aidl @@ -0,0 +1,18 @@ +/* Copyright 2018, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.audiopolicy; + +parcelable AudioVolumeGroup; diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroup.java b/media/java/android/media/audiopolicy/AudioVolumeGroup.java new file mode 100644 index 0000000000000..0b4ba937ad29e --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioVolumeGroup.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.audiopolicy; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.media.AudioAttributes; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.util.Arrays; +import java.util.List; + +/** + * A class to create the association between different playback attributes + * (e.g. media, mapping direction) to a single volume control. + * @hide + */ +@SystemApi +public final class AudioVolumeGroup implements Parcelable { + /** + * Unique identifier of a volume group. + */ + private int mId; + /** + * human-readable name of this volume group. + */ + private final String mName; + + private final AudioAttributes[] mAudioAttributes; + private int[] mLegacyStreamTypes; + + /** + * @param name of the volume group + * @param id of the volume group + * @param followers {@link AudioProductStrategies} strategy following this volume group + */ + AudioVolumeGroup(@NonNull String name, int id, + @NonNull AudioAttributes[] audioAttributes, + @NonNull int[] legacyStreamTypes) { + Preconditions.checkNotNull(name, "name must not be null"); + Preconditions.checkNotNull(audioAttributes, "audioAttributes must not be null"); + Preconditions.checkNotNull(legacyStreamTypes, "legacyStreamTypes must not be null"); + mName = name; + mId = id; + mAudioAttributes = audioAttributes; + mLegacyStreamTypes = legacyStreamTypes; + } + + @Override + public boolean equals(@NonNull Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AudioVolumeGroup thatAvg = (AudioVolumeGroup) o; + + return mName == thatAvg.mName && mId == thatAvg.mId + && mAudioAttributes.equals(thatAvg.mAudioAttributes); + } + + /** + * @return List of {@link AudioAttributes} involved in this {@link AudioVolumeGroup}. + */ + public List getAudioAttributes() { + return Arrays.asList(mAudioAttributes); + } + + /** + * @return the stream types involved in this {@link AudioVolumeGroup}. + */ + public @NonNull int[] getLegacyStreamTypes() { + return mLegacyStreamTypes; + } + + /** + * @return human-readable name of this volume group. + */ + public @NonNull String name() { + return mName; + } + + /** + * @return the volume group unique identifier id. + */ + public int getId() { + return mId; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mName); + dest.writeInt(mId); + dest.writeInt(mAudioAttributes.length); + for (AudioAttributes attributes : mAudioAttributes) { + attributes.writeToParcel(dest, flags | AudioAttributes.FLATTEN_TAGS/*flags*/); + } + dest.writeInt(mLegacyStreamTypes.length); + for (int streamType : mLegacyStreamTypes) { + dest.writeInt(streamType); + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public @NonNull AudioVolumeGroup createFromParcel(@NonNull Parcel in) { + Preconditions.checkNotNull(in, "in Parcel must not be null"); + String name = in.readString(); + int id = in.readInt(); + int nbAttributes = in.readInt(); + AudioAttributes[] audioAttributes = new AudioAttributes[nbAttributes]; + for (int index = 0; index < nbAttributes; index++) { + audioAttributes[index] = AudioAttributes.CREATOR.createFromParcel(in); + } + int nbStreamTypes = in.readInt(); + int[] streamTypes = new int[nbStreamTypes]; + for (int index = 0; index < nbStreamTypes; index++) { + streamTypes[index] = in.readInt(); + } + return new AudioVolumeGroup(name, id, audioAttributes, streamTypes); + } + + @Override + public @NonNull AudioVolumeGroup[] newArray(int size) { + return new AudioVolumeGroup[size]; + } + }; + + @Override + public @NonNull String toString() { + StringBuilder s = new StringBuilder(); + s.append("\n Name: "); + s.append(mName); + s.append(" Id: "); + s.append(Integer.toString(mId)); + + s.append("\n Supported Audio Attributes:"); + for (AudioAttributes attribute : mAudioAttributes) { + s.append("\n -"); + s.append(attribute.toString()); + } + s.append("\n Supported Legacy Stream Types: { "); + for (int legacyStreamType : mLegacyStreamTypes) { + s.append(Integer.toString(legacyStreamType)); + s.append(" "); + } + s.append("}"); + return s.toString(); + } +} diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java new file mode 100644 index 0000000000000..074188e01000c --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.audiopolicy; + +import android.annotation.NonNull; +import android.media.AudioManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; + +import com.android.internal.util.Preconditions; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * The AudioVolumeGroupChangeHandler handles AudioManager.OnAudioVolumeGroupChangedListener + * callbacks posted from JNI + * + * TODO: Make use of Executor of callbacks. + * @hide + */ +public class AudioVolumeGroupChangeHandler { + private Handler mHandler; + private HandlerThread mHandlerThread; + private final ArrayList mListeners = + new ArrayList(); + + private static final String TAG = "AudioVolumeGroupChangeHandler"; + + private static final int AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED = 1000; + private static final int AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER = 4; + + /** + * Accessed by native methods: JNI Callback context. + */ + @SuppressWarnings("unused") + private long mJniCallback; + + /** + * Initialization + */ + public void init() { + synchronized (this) { + if (mHandler != null) { + return; + } + // create a new thread for our new event handler + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + + if (mHandlerThread.getLooper() == null) { + mHandler = null; + return; + } + mHandler = new Handler(mHandlerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + ArrayList listeners; + synchronized (this) { + if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) { + listeners = + new ArrayList(); + if (mListeners.contains(msg.obj)) { + listeners.add( + (AudioManager.VolumeGroupCallback) msg.obj); + } + } else { + listeners = mListeners; + } + } + if (listeners.isEmpty()) { + return; + } + + switch (msg.what) { + case AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED: + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).onAudioVolumeGroupChanged((int) msg.arg1, + (int) msg.arg2); + } + break; + + default: + break; + } + } + }; + native_setup(new WeakReference(this)); + } + } + + private native void native_setup(Object moduleThis); + + @Override + protected void finalize() { + native_finalize(); + if (mHandlerThread.isAlive()) { + mHandlerThread.quit(); + } + } + private native void native_finalize(); + + /** + * @param cb the {@link AudioManager.VolumeGroupCallback} to register + */ + public void registerListener(@NonNull AudioManager.VolumeGroupCallback cb) { + Preconditions.checkNotNull(cb, "volume group callback shall not be null"); + synchronized (this) { + mListeners.add(cb); + } + if (mHandler != null) { + Message m = mHandler.obtainMessage( + AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER, 0, 0, cb); + mHandler.sendMessage(m); + } + } + + /** + * @param cb the {@link AudioManager.VolumeGroupCallback} to unregister + */ + public void unregisterListener(@NonNull AudioManager.VolumeGroupCallback cb) { + Preconditions.checkNotNull(cb, "volume group callback shall not be null"); + synchronized (this) { + mListeners.remove(cb); + } + } + + Handler handler() { + return mHandler; + } + + @SuppressWarnings("unused") + private static void postEventFromNative(Object moduleRef, + int what, int arg1, int arg2, Object obj) { + AudioVolumeGroupChangeHandler eventHandler = + (AudioVolumeGroupChangeHandler) ((WeakReference) moduleRef).get(); + if (eventHandler == null) { + return; + } + + if (eventHandler != null) { + Handler handler = eventHandler.handler(); + if (handler != null) { + Message m = handler.obtainMessage(what, arg1, arg2, obj); + if (what != AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) { + handler.removeMessages(what); + } + handler.sendMessage(m); + } + } + } +} diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroups.aidl b/media/java/android/media/audiopolicy/AudioVolumeGroups.aidl new file mode 100644 index 0000000000000..918cac39f19ac --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioVolumeGroups.aidl @@ -0,0 +1,18 @@ +/* Copyright 2018, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.audiopolicy; + +parcelable AudioVolumeGroups; diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroups.java b/media/java/android/media/audiopolicy/AudioVolumeGroups.java new file mode 100644 index 0000000000000..301bec7a10c40 --- /dev/null +++ b/media/java/android/media/audiopolicy/AudioVolumeGroups.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.audiopolicy; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.media.AudioSystem; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * @hide + * A class to encapsulate a collection of {@link AudioVolumeGroup}. + */ +@SystemApi +public final class AudioVolumeGroups implements Iterable, Parcelable { + + private final ArrayList mAudioVolumeGroupList; + + private static final String TAG = "AudioVolumeGroups"; + + /** + * Volume group value to use when introspection API fails. + */ + public static final int DEFAULT_VOLUME_GROUP = -1; + + public AudioVolumeGroups() { + ArrayList avgList = new ArrayList(); + int status = native_list_audio_volume_groups(avgList); + if (status != AudioSystem.SUCCESS) { + Log.w(TAG, ": listAudioVolumeGroups failed"); + } + mAudioVolumeGroupList = avgList; + } + + private AudioVolumeGroups(@NonNull ArrayList audioVolumeGroupList) { + Preconditions.checkNotNull(audioVolumeGroupList, "audioVolumeGroupList must not be null"); + mAudioVolumeGroupList = audioVolumeGroupList; + } + + /** + * @return number of {@link AudioProductStrategy} objects + */ + public int size() { + return mAudioVolumeGroupList.size(); + } + + /** + * Returns an {@link Iterator} + */ + @Override + public Iterator iterator() { + return mAudioVolumeGroupList.iterator(); + } + + @Override + public boolean equals(@NonNull Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AudioVolumeGroups that = (AudioVolumeGroups) o; + + return mAudioVolumeGroupList.equals(that.mAudioVolumeGroupList); + } + + /** + * @return the matching {@link AudioVolumeGroup} objects with the given id, + * null object if not found. + */ + public @Nullable AudioVolumeGroup getById(int volumeGroupId) { + for (final AudioVolumeGroup avg : this) { + if (avg.getId() == volumeGroupId) { + return avg; + } + } + Log.e(TAG, ": invalid volume group id: " + volumeGroupId + " requested"); + return null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(size()); + for (final AudioVolumeGroup volumeGroup : this) { + volumeGroup.writeToParcel(dest, flags); + } + } + + private static native int native_list_audio_volume_groups( + ArrayList groups); + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public @NonNull AudioVolumeGroups createFromParcel(@NonNull Parcel in) { + Preconditions.checkNotNull(in, "in Parcel must not be null"); + ArrayList avgList = new ArrayList(); + int size = in.readInt(); + for (int index = 0; index < size; index++) { + avgList.add(AudioVolumeGroup.CREATOR.createFromParcel(in)); + } + return new AudioVolumeGroups(avgList); + } + + @Override + public @NonNull AudioVolumeGroups[] newArray(int size) { + return new AudioVolumeGroups[size]; + } + }; +}