diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java new file mode 100644 index 0000000000000..0e9c99bf90d91 --- /dev/null +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 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; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * @hide + * AudioDeviceVolumeManager provides access to audio device volume control. + */ +public class AudioDeviceVolumeManager { + + // define when using Log.* + //private static final String TAG = "AudioDeviceVolumeManager"; + private static IAudioService sService; + + private final String mPackageName; + + public AudioDeviceVolumeManager(Context context) { + mPackageName = context.getApplicationContext().getOpPackageName(); + } + + /** + * @hide + * Interface to receive volume changes on a device that behaves in absolute volume mode. + * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor, + * OnAudioDeviceVolumeChangeListener) + * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor, + * OnAudioDeviceVolumeChangeListener) + */ + public interface OnAudioDeviceVolumeChangedListener { + /** + * Called the device for the given audio device has changed. + * @param device the audio device whose volume has changed + * @param vol the new volume for the device + */ + void onAudioDeviceVolumeChanged( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo vol); + } + + static class ListenerInfo { + final @NonNull OnAudioDeviceVolumeChangedListener mListener; + final @NonNull Executor mExecutor; + final @NonNull AudioDeviceAttributes mDevice; + + ListenerInfo(@NonNull OnAudioDeviceVolumeChangedListener listener, @NonNull Executor exe, + @NonNull AudioDeviceAttributes device) { + mListener = listener; + mExecutor = exe; + mDevice = device; + } + } + + private final Object mDeviceVolumeListenerLock = new Object(); + /** + * List of listeners for volume changes, the associated device, and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mDeviceVolumeListenerLock") + private @Nullable ArrayList mDeviceVolumeListeners; + + @GuardedBy("mDeviceVolumeListenerLock") + private DeviceVolumeDispatcherStub mDeviceVolumeDispatcherStub; + + final class DeviceVolumeDispatcherStub extends IAudioDeviceVolumeDispatcher.Stub { + /** + * Register / unregister the stub + * @param register true for registering, false for unregistering + * @param device device for which volume is monitored + */ + public void register(boolean register, @NonNull AudioDeviceAttributes device, + @NonNull List volumes) { + try { + getService().registerDeviceVolumeDispatcherForAbsoluteVolume(register, + this, mPackageName, + Objects.requireNonNull(device), Objects.requireNonNull(volumes)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void dispatchDeviceVolumeChanged( + @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo vol) { + final ArrayList volumeListeners; + synchronized (mDeviceVolumeListenerLock) { + volumeListeners = (ArrayList) mDeviceVolumeListeners.clone(); + } + for (ListenerInfo listenerInfo : volumeListeners) { + if (listenerInfo.mDevice.equals(device)) { + listenerInfo.mExecutor.execute( + () -> listenerInfo.mListener.onAudioDeviceVolumeChanged(device, vol)); + } + } + } + } + + /** + * @hide + * Configures a device to use absolute volume model, and registers a listener for receiving + * volume updates to apply on that device + * @param device the audio device set to absolute volume mode + * @param volume the type of volume this device responds to + * @param executor the Executor used for receiving volume updates through the listener + * @param vclistener the callback for volume updates + */ + @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, + android.Manifest.permission.BLUETOOTH_PRIVILEGED }) + public void setDeviceAbsoluteVolumeBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull VolumeInfo volume, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { + final ArrayList volumes = new ArrayList<>(1); + volumes.add(volume); + setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener); + } + + /** + * @hide + * Configures a device to use absolute volume model applied to different volume types, and + * registers a listener for receiving volume updates to apply on that device + * @param device the audio device set to absolute multi-volume mode + * @param volumes the list of volumes the given device responds to + * @param executor the Executor used for receiving volume updates through the listener + * @param vclistener the callback for volume updates + */ + @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, + android.Manifest.permission.BLUETOOTH_PRIVILEGED }) + public void setDeviceAbsoluteMultiVolumeBehavior( + @NonNull AudioDeviceAttributes device, + @NonNull List volumes, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { + Objects.requireNonNull(device); + Objects.requireNonNull(volumes); + Objects.requireNonNull(executor); + Objects.requireNonNull(vclistener); + + // TODO verify not already registered + //final ListenerInfo listenerInfo = new ListenerInfo(vclistener, executor, device); + synchronized (mDeviceVolumeListenerLock) { + if (mDeviceVolumeListeners == null) { + mDeviceVolumeListeners = new ArrayList<>(); + } + if (mDeviceVolumeListeners.size() == 0) { + if (mDeviceVolumeDispatcherStub == null) { + mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub(); + } + } + mDeviceVolumeDispatcherStub.register(true, device, volumes); + } + } + + private static IAudioService getService() { + if (sService != null) { + return sService; + } + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + sService = IAudioService.Stub.asInterface(b); + return sService; + } +} diff --git a/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl b/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl new file mode 100644 index 0000000000000..65633fe6b104b --- /dev/null +++ b/media/java/android/media/IAudioDeviceVolumeDispatcher.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 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; + +import android.media.AudioDeviceAttributes; +import android.media.VolumeInfo; + +/** + * AIDL for the AudioService to signal audio device volume changes. + * + * {@hide} + */ +oneway interface IAudioDeviceVolumeDispatcher { + + void dispatchDeviceVolumeChanged(in AudioDeviceAttributes device, in VolumeInfo vol); + +} diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index a6fdf6cedd55d..2c9f015cf4ce6 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -26,6 +26,7 @@ import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.BluetoothProfileConnectionInfo; +import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; @@ -44,6 +45,7 @@ import android.media.ISpatializerOutputCallback; import android.media.IVolumeController; import android.media.IVolumeController; import android.media.PlayerBase; +import android.media.VolumeInfo; import android.media.VolumePolicy; import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; @@ -456,6 +458,8 @@ interface IAudioService { boolean isVolumeFixed(); + VolumeInfo getDefaultVolumeInfo(); + boolean isPstnCallAudioInterceptable(); oneway void muteAwaitConnection(in int[] usagesToMute, in AudioDeviceAttributes dev, @@ -474,6 +478,7 @@ interface IAudioService { boolean sendFocusLoss(in AudioFocusInfo focusLoser, in IAudioPolicyCallback apcb); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)") void addAssistantServicesUids(in int[] assistantUID); @@ -488,4 +493,10 @@ interface IAudioService { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)") int[] getActiveAssistantServiceUids(); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)") + void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register, + in IAudioDeviceVolumeDispatcher cb, + in String packageName, + in AudioDeviceAttributes device, in List volumes); } diff --git a/media/java/android/media/VolumeInfo.aidl b/media/java/android/media/VolumeInfo.aidl new file mode 100644 index 0000000000000..aa82f52459edf --- /dev/null +++ b/media/java/android/media/VolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2022, 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; + +parcelable VolumeInfo; diff --git a/media/java/android/media/VolumeInfo.java b/media/java/android/media/VolumeInfo.java new file mode 100644 index 0000000000000..c61b0e57db1a2 --- /dev/null +++ b/media/java/android/media/VolumeInfo.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2022 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.audiopolicy.AudioVolumeGroup; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import java.util.List; +import java.util.Objects; + +/** + * @hide + * A class to represent type of volume information. + * Can be used to represent volume associated with a stream type or {@link AudioVolumeGroup}. + * Volume index is optional when used to represent a category of volume. + * Index ranges are supported too, making the representation of volume changes agnostic to the + * range (e.g. can be used to map BT A2DP absolute volume range to internal range). + * + * Note: this class is not yet part of the SystemApi but is intended to be gradually introduced + * particularly in parts of the audio framework that suffer from code ambiguity when + * dealing with different volume ranges / units. + */ +public final class VolumeInfo implements Parcelable { + private static final String TAG = "VolumeInfo"; + + private final boolean mUsesStreamType; // false implies AudioVolumeGroup is used + private final boolean mIsMuted; + private final int mVolIndex; + private final int mMinVolIndex; + private final int mMaxVolIndex; + private final int mVolGroupId; + private final int mStreamType; + + private static IAudioService sService; + private static VolumeInfo sDefaultVolumeInfo; + + private VolumeInfo(boolean usesStreamType, boolean isMuted, int volIndex, + int minVolIndex, int maxVolIndex, + int volGroupId, int streamType) { + mUsesStreamType = usesStreamType; + mIsMuted = isMuted; + mVolIndex = volIndex; + mMinVolIndex = minVolIndex; + mMaxVolIndex = maxVolIndex; + mVolGroupId = volGroupId; + mStreamType = streamType; + } + + /** + * Indicates whether this instance has a stream type associated to it. + * Note this method returning true implies {@link #hasVolumeGroup()} returns false. + * (e.g. {@link AudioManager#STREAM_MUSIC}). + * @return true if it has stream type information + */ + public boolean hasStreamType() { + return mUsesStreamType; + } + + /** + * Returns the associated stream type, or will throw if {@link #hasStreamType()} returned false. + * @return a stream type value, see AudioManager.STREAM_* + */ + public int getStreamType() { + if (!mUsesStreamType) { + throw new IllegalStateException("VolumeInfo doesn't use stream types"); + } + return mStreamType; + } + + /** + * Indicates whether this instance has a {@link AudioVolumeGroup} associated to it. + * Note this method returning true implies {@link #hasStreamType()} returns false. + * @return true if it has volume group information + */ + public boolean hasVolumeGroup() { + return !mUsesStreamType; + } + + /** + * Returns the associated volume group, or will throw if {@link #hasVolumeGroup()} returned + * false. + * @return the volume group corresponding to this VolumeInfo, or null if an error occurred + * in the volume group management + */ + public @Nullable AudioVolumeGroup getVolumeGroup() { + if (mUsesStreamType) { + throw new IllegalStateException("VolumeInfo doesn't use AudioVolumeGroup"); + } + List volGroups = AudioVolumeGroup.getAudioVolumeGroups(); + for (AudioVolumeGroup group : volGroups) { + if (group.getId() == mVolGroupId) { + return group; + } + } + return null; + } + + /** + * Returns whether this instance is conveying a mute state. + * @return true if the volume state is muted + */ + public boolean isMuted() { + return mIsMuted; + } + + /** + * A value used to express no volume index has been set. + */ + public static final int INDEX_NOT_SET = -100; + + /** + * Returns the volume index. + * @return a volume index, or {@link #INDEX_NOT_SET} if no index was set, in which case this + * instance is used to express a volume representation type (stream vs group) and + * optionally its volume range + */ + public int getVolumeIndex() { + return mVolIndex; + } + + /** + * Returns the minimum volume index. + * @return the minimum volume index, or {@link #INDEX_NOT_SET} if no minimum index was set. + */ + public int getMinVolumeIndex() { + return mMinVolIndex; + } + + /** + * Returns the maximum volume index. + * @return the maximum volume index, or {@link #INDEX_NOT_SET} if no maximum index was + * set. + */ + public int getMaxVolumeIndex() { + return mMaxVolIndex; + } + + /** + * Returns the default info for the platform, typically initialized + * to STREAM_MUSIC with min/max initialized to the associated range + * @return the default VolumeInfo for the device + */ + public static @NonNull VolumeInfo getDefaultVolumeInfo() { + if (sService == null) { + IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); + sService = IAudioService.Stub.asInterface(b); + } + if (sDefaultVolumeInfo == null) { + try { + sDefaultVolumeInfo = sService.getDefaultVolumeInfo(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getDefaultVolumeInfo", e); + // return a valid value, but don't cache it + return new VolumeInfo.Builder(AudioManager.STREAM_MUSIC).build(); + } + } + return sDefaultVolumeInfo; + } + + /** + * The builder class for creating and initializing, or copying and modifying VolumeInfo + * instances + */ + public static final class Builder { + private boolean mUsesStreamType = true; // false implies AudioVolumeGroup is used + private int mStreamType = AudioManager.STREAM_MUSIC; + private boolean mIsMuted = false; + private int mVolIndex = INDEX_NOT_SET; + private int mMinVolIndex = INDEX_NOT_SET; + private int mMaxVolIndex = INDEX_NOT_SET; + private int mVolGroupId = -Integer.MIN_VALUE; + + /** + * Builder constructor for stream type-based VolumeInfo + */ + public Builder(int streamType) { + // TODO validate stream type + mUsesStreamType = true; + mStreamType = streamType; + } + + /** + * Builder constructor for volume group-based VolumeInfo + */ + public Builder(@NonNull AudioVolumeGroup volGroup) { + Objects.requireNonNull(volGroup); + mUsesStreamType = false; + mStreamType = -Integer.MIN_VALUE; + mVolGroupId = volGroup.getId(); + } + + /** + * Builder constructor to copy a given VolumeInfo. + * Note you can't change the stream type or volume group later. + */ + public Builder(@NonNull VolumeInfo info) { + Objects.requireNonNull(info); + mUsesStreamType = info.mUsesStreamType; + mStreamType = info.mStreamType; + mIsMuted = info.mIsMuted; + mVolIndex = info.mVolIndex; + mMinVolIndex = info.mMinVolIndex; + mMaxVolIndex = info.mMaxVolIndex; + mVolGroupId = info.mVolGroupId; + } + + /** + * Sets whether the volume is in a muted state + * @param isMuted + * @return the same builder instance + */ + public @NonNull Builder setMuted(boolean isMuted) { + mIsMuted = isMuted; + return this; + } + + /** + * Sets the volume index + * @param volIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown + * @return the same builder instance + */ + // TODO should we allow muted true + volume index set? (useful when toggling mute on/off?) + public @NonNull Builder setVolumeIndex(int volIndex) { + if (volIndex != INDEX_NOT_SET && volIndex < 0) { + throw new IllegalArgumentException("Volume index cannot be negative"); + } + mVolIndex = volIndex; + return this; + } + + /** + * Sets the minimum volume index + * @param minIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown + * @return the same builder instance + */ + public @NonNull Builder setMinVolumeIndex(int minIndex) { + if (minIndex != INDEX_NOT_SET && minIndex < 0) { + throw new IllegalArgumentException("Min volume index cannot be negative"); + } + mMinVolIndex = minIndex; + return this; + } + + /** + * Sets the maximum volume index + * @param maxIndex a 0 or greater value, or {@link #INDEX_NOT_SET} if unknown + * @return the same builder instance + */ + public @NonNull Builder setMaxVolumeIndex(int maxIndex) { + if (maxIndex != INDEX_NOT_SET && maxIndex < 0) { + throw new IllegalArgumentException("Max volume index cannot be negative"); + } + mMaxVolIndex = maxIndex; + return this; + } + + /** + * Builds the VolumeInfo with the data given to the builder + * @return the new VolumeInfo instance + */ + public @NonNull VolumeInfo build() { + if (mVolIndex != INDEX_NOT_SET) { + if (mMinVolIndex != INDEX_NOT_SET && mVolIndex < mMinVolIndex) { + throw new IllegalArgumentException("Volume index:" + mVolIndex + + " lower than min index:" + mMinVolIndex); + } + if (mMaxVolIndex != INDEX_NOT_SET && mVolIndex > mMaxVolIndex) { + throw new IllegalArgumentException("Volume index:" + mVolIndex + + " greater than max index:" + mMaxVolIndex); + } + } + if (mMinVolIndex != INDEX_NOT_SET && mMaxVolIndex != INDEX_NOT_SET + && mMinVolIndex > mMaxVolIndex) { + throw new IllegalArgumentException("Min volume index:" + mMinVolIndex + + " greater than max index:" + mMaxVolIndex); + } + return new VolumeInfo(mUsesStreamType, mIsMuted, + mVolIndex, mMinVolIndex, mMaxVolIndex, + mVolGroupId, mStreamType); + } + } + + //----------------------------------------------- + // Parcelable + @Override + public int hashCode() { + return Objects.hash(mUsesStreamType, mStreamType, mIsMuted, + mVolIndex, mMinVolIndex, mMaxVolIndex, mVolGroupId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VolumeInfo that = (VolumeInfo) o; + return ((mUsesStreamType == that.mUsesStreamType) + && (mStreamType == that.mStreamType) + && (mIsMuted == that.mIsMuted) + && (mVolIndex == that.mVolIndex) + && (mMinVolIndex == that.mMinVolIndex) + && (mMaxVolIndex == that.mMaxVolIndex) + && (mVolGroupId == that.mVolGroupId)); + } + + @Override + public String toString() { + return new String("VolumeInfo:" + + (mUsesStreamType ? (" streamType:" + mStreamType) + : (" volGroupId" + mVolGroupId)) + + " muted:" + mIsMuted + + ((mVolIndex != INDEX_NOT_SET) ? (" volIndex:" + mVolIndex) : "") + + ((mMinVolIndex != INDEX_NOT_SET) ? (" min:" + mMinVolIndex) : "") + + ((mMaxVolIndex != INDEX_NOT_SET) ? (" max:" + mMaxVolIndex) : "")); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mUsesStreamType); + dest.writeInt(mStreamType); + dest.writeBoolean(mIsMuted); + dest.writeInt(mVolIndex); + dest.writeInt(mMinVolIndex); + dest.writeInt(mMaxVolIndex); + dest.writeInt(mVolGroupId); + } + + private VolumeInfo(@NonNull Parcel in) { + mUsesStreamType = in.readBoolean(); + mStreamType = in.readInt(); + mIsMuted = in.readBoolean(); + mVolIndex = in.readInt(); + mMinVolIndex = in.readInt(); + mMaxVolIndex = in.readInt(); + mVolGroupId = in.readInt(); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + /** + * Rebuilds a VolumeInfo previously stored with writeToParcel(). + * @param p Parcel object to read the VolumeInfo from + * @return a new VolumeInfo created from the data in the parcel + */ + public VolumeInfo createFromParcel(Parcel p) { + return new VolumeInfo(p); + } + + public VolumeInfo[] newArray(int size) { + return new VolumeInfo[size]; + } + }; +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3e5e4357b8f4b..807293fdae500 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -36,6 +36,7 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -87,6 +88,7 @@ import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.BluetoothProfileConnectionInfo; +import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; @@ -108,6 +110,7 @@ import android.media.MediaMetrics; import android.media.MediaRecorder.AudioSource; import android.media.PlayerBase; import android.media.Spatializer; +import android.media.VolumeInfo; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; @@ -4246,6 +4249,24 @@ public class AudioService extends IAudioService.Stub return (mStreamStates[streamType].getIndex(device) + 5) / 10; } + /** + * Default VolumeInfo returned by {@link VolumeInfo#getDefaultVolumeInfo()} + * Lazily initialized in {@link #getDefaultVolumeInfo()} + */ + static VolumeInfo sDefaultVolumeInfo; + + /** @see VolumeInfo#getDefaultVolumeInfo() */ + public VolumeInfo getDefaultVolumeInfo() { + if (sDefaultVolumeInfo == null) { + sDefaultVolumeInfo = new VolumeInfo.Builder(AudioSystem.STREAM_MUSIC) + .setMinVolumeIndex(getStreamMinVolume(AudioSystem.STREAM_MUSIC)) + .setMaxVolumeIndex(getStreamMaxVolume(AudioSystem.STREAM_MUSIC)) + .setMuted(false) + .build(); + } + return sDefaultVolumeInfo; + } + /** @see AudioManager#getUiSoundsStreamType() * TODO(b/181140246): when using VolumeGroup alias, we are lacking configurability for * UI Sounds identification. @@ -6309,6 +6330,37 @@ public class AudioService extends IAudioService.Stub 0 /*delay*/); } + /** + * @see AudioDeviceVolumeManager#setDeviceAbsoluteMultiVolumeBehavior + * @param cb + * @param attr + * @param volumes + */ + @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, + android.Manifest.permission.BLUETOOTH_PRIVILEGED }) + public void registerDeviceVolumeDispatcherForAbsoluteVolume(boolean register, + IAudioDeviceVolumeDispatcher cb, String packageName, + AudioDeviceAttributes device, List volumes) { + // verify permissions + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + != PackageManager.PERMISSION_GRANTED + && mContext.checkCallingOrSelfPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Missing MODIFY_AUDIO_ROUTING or BLUETOOTH_PRIVILEGED permissions"); + } + // verify arguments + Objects.requireNonNull(device); + Objects.requireNonNull(volumes); + + // current implementation maps this call to existing abs volume API of AudioManager + // TODO implement the volume/device listener through IAudioDeviceVolumeDispatcher + final int volumeBehavior = volumes.size() == 1 + ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE + : AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE; + setDeviceVolumeBehavior(device, volumeBehavior, packageName); + } + /** * @see AudioManager#setDeviceVolumeBehavior(AudioDeviceAttributes, int) * @param device the audio device to be affected