diff options
author | 2025-03-17 18:21:11 -0700 | |
---|---|---|
committer | 2025-03-17 18:21:11 -0700 | |
commit | f4cf1860278e0a522f2fe4714e1845b903bc80bc (patch) | |
tree | da38b456e0872639234840fdacaeb6c362a93b16 | |
parent | 65b2d14c9d58255bbef5b460573b70adb737953d (diff) | |
parent | ad09eb6eb16fcedcbb14e9fba0a08da7c5232a3c (diff) |
Merge changes from topic "VOL_GROUP_CB_ENFORCMENT" into main
* changes:
Add permission to volume callback APIs
Add MODIFY_AUDIO_SETTINGS_PRIVILEGED permission check to volume callback
AudioService: move volume callback on AudioService
21 files changed, 449 insertions, 638 deletions
diff --git a/Android.bp b/Android.bp index 127556f8e075..e7c20418bcef 100644 --- a/Android.bp +++ b/Android.bp @@ -427,6 +427,7 @@ java_defaults { "framework-permission-aidl-java", "spatializer-aidl-java", "audiopolicy-aidl-java", + "volumegroupcallback-aidl-java", "sounddose-aidl-java", "modules-utils-expresslog", "perfetto_trace_javastream_protos_jarjar", diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt index d7c409fd54b4..4c3e5dce7a95 100644 --- a/boot/boot-image-profile.txt +++ b/boot/boot-image-profile.txt @@ -28829,7 +28829,6 @@ Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup; Landroid/media/audiopolicy/AudioProductStrategy; Landroid/media/audiopolicy/AudioVolumeGroup$1; Landroid/media/audiopolicy/AudioVolumeGroup; -Landroid/media/audiopolicy/AudioVolumeGroupChangeHandler; Landroid/media/audiopolicy/IAudioPolicyCallback$Stub$Proxy; Landroid/media/audiopolicy/IAudioPolicyCallback$Stub; Landroid/media/audiopolicy/IAudioPolicyCallback; diff --git a/boot/preloaded-classes b/boot/preloaded-classes index 7f4b3244c164..048687781774 100644 --- a/boot/preloaded-classes +++ b/boot/preloaded-classes @@ -5509,7 +5509,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup android.media.audiopolicy.AudioProductStrategy android.media.audiopolicy.AudioVolumeGroup$1 android.media.audiopolicy.AudioVolumeGroup -android.media.audiopolicy.AudioVolumeGroupChangeHandler android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy android.media.audiopolicy.IAudioPolicyCallback$Stub android.media.audiopolicy.IAudioPolicyCallback diff --git a/config/preloaded-classes b/config/preloaded-classes index 707acb00b102..3f0e00be75a7 100644 --- a/config/preloaded-classes +++ b/config/preloaded-classes @@ -5514,7 +5514,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup android.media.audiopolicy.AudioProductStrategy android.media.audiopolicy.AudioVolumeGroup$1 android.media.audiopolicy.AudioVolumeGroup -android.media.audiopolicy.AudioVolumeGroupChangeHandler android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy android.media.audiopolicy.IAudioPolicyCallback$Stub android.media.audiopolicy.IAudioPolicyCallback diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 746035462329..95b9b49dae3d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -7442,7 +7442,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback); - method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); + method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes); method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener); @@ -7473,7 +7473,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback); - method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); + method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); field public static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION"; field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1 field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4 diff --git a/core/jni/Android.bp b/core/jni/Android.bp index bfa0aa9638a9..7ed73d7668b9 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -210,7 +210,6 @@ cc_library_shared_for_libandroid_runtime { "android_media_AudioAttributes.cpp", "android_media_AudioProductStrategies.cpp", "android_media_AudioVolumeGroups.cpp", - "android_media_AudioVolumeGroupCallback.cpp", "android_media_DeviceCallback.cpp", "android_media_MediaMetricsJNI.cpp", "android_media_MicrophoneInfo.cpp", @@ -311,6 +310,7 @@ cc_library_shared_for_libandroid_runtime { "audioflinger-aidl-cpp", "audiopolicy-types-aidl-cpp", "spatializer-aidl-cpp", + "volumegroupcallback-aidl-cpp", "av-types-aidl-cpp", "android.hardware.camera.device@3.2", "camera_platform_flags_c_lib", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b2b826391e1d..1ff07745e904 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -101,7 +101,6 @@ 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_ImageReader(JNIEnv *env); extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_MicrophoneInfo(JNIEnv *env); @@ -1660,7 +1659,6 @@ static const RegJNIRec gRegJNI[] = { 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_ImageReader), REG_JNI(register_android_media_ImageWriter), REG_JNI(register_android_media_MediaMetrics), diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index b679688959b1..1bbf811dc373 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -20,16 +20,17 @@ #include <atomic> #define LOG_TAG "AudioSystem-JNI" +#include <android-base/properties.h> #include <android/binder_ibinder_jni.h> #include <android/binder_libbinder.h> #include <android/media/AudioVibratorInfo.h> +#include <android/media/INativeAudioVolumeGroupCallback.h> #include <android/media/INativeSpatializerCallback.h> #include <android/media/ISpatializer.h> #include <android/media/audio/common/AudioConfigBase.h> #include <android_media_audiopolicy.h> #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> -#include <android-base/properties.h> #include <binder/IBinder.h> #include <jni.h> #include <media/AidlConversion.h> @@ -41,14 +42,14 @@ #include <nativehelper/ScopedLocalRef.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/jni_macros.h> +#include <sys/system_properties.h> #include <system/audio.h> #include <system/audio_policy.h> -#include <sys/system_properties.h> #include <utils/Log.h> +#include <memory> #include <optional> #include <sstream> -#include <memory> #include <vector> #include "android_media_AudioAttributes.h" @@ -59,8 +60,8 @@ #include "android_media_AudioFormat.h" #include "android_media_AudioMixerAttributes.h" #include "android_media_AudioProfile.h" -#include "android_media_MicrophoneInfo.h" #include "android_media_JNIUtils.h" +#include "android_media_MicrophoneInfo.h" #include "android_util_Binder.h" #include "core_jni_helpers.h" @@ -3442,6 +3443,21 @@ static void android_media_AudioSystem_triggerSystemPropertyUpdate(JNIEnv *env, } } +static int android_media_AudioSystem_registerAudioVolumeGroupCallback( + JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) { + sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback = + interface_cast<media::INativeAudioVolumeGroupCallback>( + ibinderForJavaObject(env, jIAudioVolumeGroupCallback)); + return AudioSystem::addAudioVolumeGroupCallback(nIAudioVolumeGroupCallback); +} + +static int android_media_AudioSystem_unregisterAudioVolumeGroupCallback( + JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) { + sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback = + interface_cast<media::INativeAudioVolumeGroupCallback>( + ibinderForJavaObject(env, jIAudioVolumeGroupCallback)); + return AudioSystem::removeAudioVolumeGroupCallback(nIAudioVolumeGroupCallback); +} // ---------------------------------------------------------------------------- @@ -3612,6 +3628,12 @@ static const JNINativeMethod gMethods[] = { MAKE_JNI_NATIVE_METHOD("clearPreferredMixerAttributes", "(Landroid/media/AudioAttributes;II)I", android_media_AudioSystem_clearPreferredMixerAttributes), + MAKE_JNI_NATIVE_METHOD("registerAudioVolumeGroupCallback", + "(Landroid/media/INativeAudioVolumeGroupCallback;)I", + android_media_AudioSystem_registerAudioVolumeGroupCallback), + MAKE_JNI_NATIVE_METHOD("unregisterAudioVolumeGroupCallback", + "(Landroid/media/INativeAudioVolumeGroupCallback;)I", + android_media_AudioSystem_unregisterAudioVolumeGroupCallback), MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency), MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp deleted file mode 100644 index d130a4bc68fa..000000000000 --- a/core/jni/android_media_AudioVolumeGroupCallback.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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. - */ -#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code - -//#define LOG_NDEBUG 0 - -#define LOG_TAG "AudioVolumeGroupCallback-JNI" - -#include <utils/Log.h> -#include <nativehelper/JNIHelp.h> -#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<JNIAudioVolumeGroupCallback> setJniCallback(JNIEnv* env, - jobject thiz, - const sp<JNIAudioVolumeGroupCallback>& callback) -{ - Mutex::Autolock l(gLock); - sp<JNIAudioVolumeGroupCallback> 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<JNIAudioVolumeGroupCallback> 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<JNIAudioVolumeGroupCallback> 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 deleted file mode 100644 index de06549621b9..000000000000 --- a/core/jni/android_media_AudioVolumeGroupCallback.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 <system/audio.h> -#include <media/AudioSystem.h> - -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/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4aba491c291e..0a1bfd55e77f 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -19,14 +19,15 @@ package android.media; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; -import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT; -import static android.media.audio.Flags.autoPublicVolumeApiHardening; -import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; -import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO; import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; +import static android.media.audio.Flags.FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING; import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API; +import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT; +import static android.media.audio.Flags.autoPublicVolumeApiHardening; +import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; +import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.Manifest; @@ -65,7 +66,7 @@ import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; -import android.media.audiopolicy.AudioVolumeGroupChangeHandler; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.projection.MediaProjection; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -128,8 +129,6 @@ 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(); private static WeakReference<Context> sContext; @@ -8761,9 +8760,13 @@ public class AudioManager { } } + //==================================================================== + // Notification of volume group changes /** + * Callback to receive updates on volume group changes, register using + * {@link AudioManager#registerVolumeGroupCallback(Executor, AudioVolumeCallback)}. + * * @hide - * Callback registered by client to be notified upon volume group change. */ @SystemApi public abstract static class VolumeGroupCallback { @@ -8774,34 +8777,69 @@ public class AudioManager { public void onAudioVolumeGroupChanged(int group, int flags) {} } - /** - * @hide - * Register an audio volume group change listener. - * @param callback the {@link VolumeGroupCallback} to register - */ + /** + * Register an audio volume group change listener. + * + * @param executor {@link Executor} to handle the callbacks + * @param callback the callback to receive the audio volume group changes + * @throws SecurityException if the caller doesn't have the required permission. + * + * @hide + */ @SystemApi - public void registerVolumeGroupCallback( - @NonNull Executor executor, + @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING) + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + 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); + mVolumeChangedListenerMgr.addListener(executor, callback, "registerVolumeGroupCallback", + () -> new AudioVolumeChangeDispatcherStub()); } - /** - * @hide - * Unregister an audio volume group change listener. - * @param callback the {@link VolumeGroupCallback} to unregister - */ + /** + * Unregister an audio volume group change listener. + * + * @param callback the {@link VolumeGroupCallback} to unregister + * + * @hide + */ @SystemApi - public void unregisterVolumeGroupCallback( - @NonNull VolumeGroupCallback callback) { - Preconditions.checkNotNull(callback, "volume group change cb must not be null"); - sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback); + @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING) + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public void unregisterVolumeGroupCallback(@NonNull VolumeGroupCallback callback) { + mVolumeChangedListenerMgr.removeListener(callback, "unregisterVolumeGroupCallback"); + } + + /** + * Manages the VolumeGroupCallback listeners and the AudioVolumeChangeDispatcherStub + */ + private final CallbackUtil.LazyListenerManager<VolumeGroupCallback> mVolumeChangedListenerMgr = + new CallbackUtil.LazyListenerManager(); + + final class AudioVolumeChangeDispatcherStub extends IAudioVolumeChangeDispatcher.Stub + implements CallbackUtil.DispatcherStub { + + @Override + public void register(boolean register) { + try { + if (register) { + getService().registerAudioVolumeCallback(this); + } else { + getService().unregisterAudioVolumeCallback(this); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onAudioVolumeGroupChanged(int group, int flags) { + mVolumeChangedListenerMgr.callListeners((listener) -> + listener.onAudioVolumeGroupChanged(group, flags)); + } } + //==================================================================== + /** * Return if an asset contains haptic channels or not. * diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index ad6f2e52fd97..4906cd3fb1e5 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2732,4 +2732,25 @@ public class AudioSystem * @hide */ public static native void triggerSystemPropertyUpdate(long handle); + + /** + * Registers the given {@link INativeAudioVolumeGroupCallback} to native audioserver. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public static native int registerAudioVolumeGroupCallback( + INativeAudioVolumeGroupCallback callback); + + /** + * Unegisters the given {@link INativeAudioVolumeGroupCallback} from native audioserver + * previously registered via {@link #registerAudioVolumeGroupCallback}. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public static native int unregisterAudioVolumeGroupCallback( + INativeAudioVolumeGroupCallback callback); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 8aadb418cf5a..c505bcee0332 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -65,6 +65,7 @@ import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; import android.media.audiopolicy.IAudioPolicyCallback; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.projection.IMediaProjection; import android.net.Uri; import android.os.PersistableBundle; @@ -446,6 +447,12 @@ interface IAudioService { boolean isAudioServerRunning(); + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher avc); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + oneway void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher avc); + int setUidDeviceAffinity(in IAudioPolicyCallback pcb, in int uid, in int[] deviceTypes, in String[] deviceAddresses); diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java deleted file mode 100644 index 022cfeeb4e43..000000000000 --- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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<AudioManager.VolumeGroupCallback> mListeners = - new ArrayList<AudioManager.VolumeGroupCallback>(); - - 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<AudioManager.VolumeGroupCallback> listeners; - synchronized (this) { - if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) { - listeners = - new ArrayList<AudioManager.VolumeGroupCallback>(); - if (mListeners.contains(msg.obj)) { - listeners.add( - (AudioManager.VolumeGroupCallback) msg.obj); - } - } else { - listeners = (ArrayList<AudioManager.VolumeGroupCallback>) - mListeners.clone(); - } - } - 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<AudioVolumeGroupChangeHandler>(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); - // Do not remove previous messages, as we would lose notification of group changes - handler.sendMessage(m); - } - } - } -} diff --git a/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl new file mode 100644 index 000000000000..e6f9024cfd1e --- /dev/null +++ b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl @@ -0,0 +1,31 @@ +/* Copyright (C) 2025 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; + +/** + * AIDL for the AudioService to signal audio volume groups changes + * + * {@hide} + */ +oneway interface IAudioVolumeChangeDispatcher { + + /** + * Called when a volume group has been changed + * @param group id of the volume group that has changed. + * @param flags one or more flags to describe the volume change. + */ + void onAudioVolumeGroupChanged(int group, int flags); +} diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml index 5c911b135a5d..466da7e66fbf 100644 --- a/media/tests/AudioPolicyTest/AndroidManifest.xml +++ b/media/tests/AudioPolicyTest/AndroidManifest.xml @@ -19,6 +19,7 @@ <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> <uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" /> diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java deleted file mode 100644 index 82394a2eb420..000000000000 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2020 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 com.android.audiopolicytest; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES; -import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.audiopolicy.AudioVolumeGroup; -import android.media.audiopolicy.AudioVolumeGroupChangeHandler; -import android.platform.test.annotations.Presubmit; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -@Presubmit -@RunWith(AndroidJUnit4.class) -public class AudioVolumeGroupChangeHandlerTest { - private static final String TAG = "AudioVolumeGroupChangeHandlerTest"; - - @Rule - public final AudioVolumesTestRule rule = new AudioVolumesTestRule(); - - private AudioManager mAudioManager; - - @Before - public void setUp() { - mAudioManager = getApplicationContext().getSystemService(AudioManager.class); - } - - @Test - public void testRegisterInvalidCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - assertThrows(NullPointerException.class, () -> { - AudioManager.VolumeGroupCallback nullCb = null; - audioAudioVolumeGroupChangedHandler.registerListener(nullCb); - }); - } - - @Test - public void testUnregisterInvalidCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final AudioVolumeGroupCallbackHelper cb = new AudioVolumeGroupCallbackHelper(); - audioAudioVolumeGroupChangedHandler.registerListener(cb); - - assertThrows(NullPointerException.class, () -> { - AudioManager.VolumeGroupCallback nullCb = null; - audioAudioVolumeGroupChangedHandler.unregisterListener(nullCb); - }); - audioAudioVolumeGroupChangedHandler.unregisterListener(cb); - } - - @Test - public void testRegisterUnregisterCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper(); - - // Should not assert, otherwise test will fail - audioAudioVolumeGroupChangedHandler.registerListener(validCb); - - // Should not assert, otherwise test will fail - audioAudioVolumeGroupChangedHandler.unregisterListener(validCb); - } - - @Test - public void testCallbackReceived() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper(); - audioAudioVolumeGroupChangedHandler.registerListener(validCb); - - List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups(); - assertTrue(audioVolumeGroups.size() > 0); - - try { - for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) { - int volumeGroupId = audioVolumeGroup.getId(); - - List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes(); - // Set the volume per attributes (if valid) and wait the callback - if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) { - // Some volume groups may not have valid attributes, used for internal - // volume management like patch/rerouting - // so bailing out strategy retrieval from attributes - continue; - } - final AudioAttributes aa = avgAttributes.get(0); - - int index = mAudioManager.getVolumeIndexForAttributes(aa); - int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa); - int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa); - - final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax); - - // Set the receiver to filter only the current group callback - validCb.setExpectedVolumeGroup(volumeGroupId); - mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/); - assertTrue(validCb.waitForExpectedVolumeGroupChanged( - AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS)); - - final int readIndex = mAudioManager.getVolumeIndexForAttributes(aa); - assertEquals(readIndex, indexForAa); - } - } finally { - audioAudioVolumeGroupChangedHandler.unregisterListener(validCb); - } - } - - @Test - public void testMultipleCallbackReceived() { - - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final int callbackCount = 10; - final List<AudioVolumeGroupCallbackHelper> validCbs = - new ArrayList<AudioVolumeGroupCallbackHelper>(); - for (int i = 0; i < callbackCount; i++) { - validCbs.add(new AudioVolumeGroupCallbackHelper()); - } - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - audioAudioVolumeGroupChangedHandler.registerListener(cb); - } - - List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups(); - assertTrue(audioVolumeGroups.size() > 0); - - try { - for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) { - int volumeGroupId = audioVolumeGroup.getId(); - - List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes(); - // Set the volume per attributes (if valid) and wait the callback - if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) { - // Some volume groups may not have valid attributes, used for internal - // volume management like patch/rerouting - // so bailing out strategy retrieval from attributes - continue; - } - AudioAttributes aa = avgAttributes.get(0); - - int index = mAudioManager.getVolumeIndexForAttributes(aa); - int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa); - int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa); - - final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax); - - // Set the receiver to filter only the current group callback - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - cb.setExpectedVolumeGroup(volumeGroupId); - } - mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/); - - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - assertTrue(cb.waitForExpectedVolumeGroupChanged( - AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS)); - } - int readIndex = mAudioManager.getVolumeIndexForAttributes(aa); - assertEquals(readIndex, indexForAa); - } - } finally { - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - audioAudioVolumeGroupChangedHandler.unregisterListener(cb); - } - } - } -} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6b3661a2a004..a8bb5231d8c0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -186,6 +186,7 @@ import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; import android.media.audiopolicy.IAudioPolicyCallback; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.media.projection.IMediaProjection; @@ -1388,6 +1389,7 @@ public class AudioService extends IAudioService.Stub mUseVolumeGroupAliases = mContext.getResources().getBoolean( com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups); + mAudioVolumeChangeHandler = new AudioVolumeChangeHandler(mAudioSystem); // Initialize volume // Priority 1 - Android Property // Priority 2 - Audio Policy Service @@ -4452,6 +4454,27 @@ public class AudioService extends IAudioService.Stub } } + //================================ + // Audio Volume Change Dispatcher + //================================ + private final AudioVolumeChangeHandler mAudioVolumeChangeHandler; + + /** @see AudioManager#registerVolumeGroupCallback(executor, callback) */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) { + super.registerAudioVolumeCallback_enforcePermission(); + mAudioVolumeChangeHandler.registerListener(callback); + } + + /** @see AudioManager#unregisterVolumeGroupCallback(callback) */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) { + super.unregisterAudioVolumeCallback_enforcePermission(); + mAudioVolumeChangeHandler.unregisterListener(callback); + } + @Override @android.annotation.EnforcePermission(anyOf = { MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING }) diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index a6267c156fb3..ced5faeeff27 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -23,6 +23,7 @@ import android.media.AudioDeviceAttributes; import android.media.AudioMixerAttributes; import android.media.AudioSystem; import android.media.IDevicesForAttributesCallback; +import android.media.INativeAudioVolumeGroupCallback; import android.media.ISoundDose; import android.media.ISoundDoseCallback; import android.media.audiopolicy.AudioMix; @@ -758,6 +759,29 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * Same as {@link AudioSystem#registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)} + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public int registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) { + return AudioSystem.registerAudioVolumeGroupCallback(callback); + } + + /** + * Same as + * {@link AudioSystem#unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public int unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) { + return AudioSystem.unregisterAudioVolumeGroupCallback(callback); + } + + /** * Part of AudioService dump * @param pw */ diff --git a/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java new file mode 100644 index 000000000000..2bb4301bb8fd --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 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 com.android.server.audio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.INativeAudioVolumeGroupCallback; +import android.media.audio.common.AudioVolumeGroupChangeEvent; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +/** + * The AudioVolumeChangeHandler handles AudioVolume callbacks invoked by native + * {@link INativeAudioVolumeGroupCallback} callback. + */ +/* private package */ class AudioVolumeChangeHandler { + private static final String TAG = "AudioVolumeChangeHandler"; + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final RemoteCallbackList<IAudioVolumeChangeDispatcher> mListeners = + new RemoteCallbackList<>(); + private final @NonNull AudioSystemAdapter mAudioSystem; + private @Nullable AudioVolumeGroupCallback mAudioVolumeGroupCallback; + + AudioVolumeChangeHandler(@NonNull AudioSystemAdapter asa) { + mAudioSystem = asa; + } + + @GuardedBy("mLock") + private void lazyInitLocked() { + mAudioVolumeGroupCallback = new AudioVolumeGroupCallback(); + mAudioSystem.registerAudioVolumeGroupCallback(mAudioVolumeGroupCallback); + } + + private void sendAudioVolumeGroupChangedToClients(int groupId, int index) { + RemoteCallbackList<IAudioVolumeChangeDispatcher> listeners; + int nbDispatchers; + synchronized (mLock) { + listeners = mListeners; + nbDispatchers = mListeners.beginBroadcast(); + } + for (int i = 0; i < nbDispatchers; i++) { + try { + listeners.getBroadcastItem(i).onAudioVolumeGroupChanged(groupId, index); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to broadcast Volume Changed event"); + } + } + synchronized (mLock) { + mListeners.finishBroadcast(); + } + } + + /** + * @param cb the {@link IAudioVolumeChangeDispatcher} to register + */ + public void registerListener(@NonNull IAudioVolumeChangeDispatcher cb) { + Preconditions.checkNotNull(cb, "Volume group callback must not be null"); + synchronized (mLock) { + if (mAudioVolumeGroupCallback == null) { + lazyInitLocked(); + } + mListeners.register(cb); + } + } + + /** + * @param cb the {@link IAudioVolumeChangeDispatcher} to unregister + */ + public void unregisterListener(@NonNull IAudioVolumeChangeDispatcher cb) { + Preconditions.checkNotNull(cb, "Volume group callback must not be null"); + synchronized (mLock) { + mListeners.unregister(cb); + } + } + + private final class AudioVolumeGroupCallback extends INativeAudioVolumeGroupCallback.Stub { + public void onAudioVolumeGroupChanged(AudioVolumeGroupChangeEvent volumeEvent) { + Slog.v(TAG, "onAudioVolumeGroupChanged volumeEvent=" + volumeEvent); + sendAudioVolumeGroupChangedToClients(volumeEvent.groupId, volumeEvent.flags); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java new file mode 100644 index 000000000000..f252a9848bbf --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 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 com.android.server.audio; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.INativeAudioVolumeGroupCallback; +import android.media.audio.common.AudioVolumeGroupChangeEvent; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.List; + +@MediumTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class AudioVolumeChangeHandlerTest { + private static final long DEFAULT_TIMEOUT_MS = 1000; + + private AudioSystemAdapter mSpyAudioSystem; + + AudioVolumeChangeHandler mAudioVolumeChangedHandler; + + private final IAudioVolumeChangeDispatcher.Stub mMockDispatcher = + mock(IAudioVolumeChangeDispatcher.Stub.class); + + @Before + public void setUp() { + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + when(mMockDispatcher.asBinder()).thenReturn(mock(IBinder.class)); + mAudioVolumeChangedHandler = new AudioVolumeChangeHandler(mSpyAudioSystem); + } + + @Test + public void registerListener_withInvalidCallback() { + IAudioVolumeChangeDispatcher.Stub nullCb = null; + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + mAudioVolumeChangedHandler.registerListener(nullCb); + }); + + assertWithMessage("Exception for invalid registration").that(thrown).hasMessageThat() + .contains("Volume group callback"); + } + + @Test + public void unregisterListener_withInvalidCallback() { + IAudioVolumeChangeDispatcher.Stub nullCb = null; + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + mAudioVolumeChangedHandler.unregisterListener(nullCb); + }); + + assertWithMessage("Exception for invalid un-registration").that(thrown).hasMessageThat() + .contains("Volume group callback"); + } + + @Test + public void registerListener() { + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + + verify(mSpyAudioSystem).registerAudioVolumeGroupCallback(any()); + } + + @Test + public void onAudioVolumeGroupChanged() throws Exception { + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent(); + volEvent.groupId = 666; + volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY; + + captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent); + + verify(mMockDispatcher, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged( + eq(volEvent.groupId), eq(volEvent.flags)); + } + + @Test + public void onAudioVolumeGroupChanged_withMultipleCallback() throws Exception { + int callbackCount = 10; + List<IAudioVolumeChangeDispatcher.Stub> validCbs = + new ArrayList<IAudioVolumeChangeDispatcher.Stub>(); + for (int i = 0; i < callbackCount; i++) { + IAudioVolumeChangeDispatcher.Stub cb = mock(IAudioVolumeChangeDispatcher.Stub.class); + when(cb.asBinder()).thenReturn(mock(IBinder.class)); + validCbs.add(cb); + } + for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) { + mAudioVolumeChangedHandler.registerListener(cb); + } + AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent(); + volEvent.groupId = 666; + volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY; + captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent); + + for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) { + verify(cb, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged( + eq(volEvent.groupId), eq(volEvent.flags)); + } + } + + private INativeAudioVolumeGroupCallback captureRegisteredNativeCallback() { + ArgumentCaptor<INativeAudioVolumeGroupCallback> nativeAudioVolumeGroupCallbackCaptor = + ArgumentCaptor.forClass(INativeAudioVolumeGroupCallback.class); + verify(mSpyAudioSystem, timeout(DEFAULT_TIMEOUT_MS)) + .registerAudioVolumeGroupCallback(nativeAudioVolumeGroupCallbackCaptor.capture()); + return nativeAudioVolumeGroupCallbackCaptor.getValue(); + } +} |