summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jean-Michel Trivi <jmtrivi@google.com> 2022-07-13 03:39:51 +0000
committer Jean-Michel Trivi <jmtrivi@google.com> 2022-07-27 19:02:25 +0000
commit118ca2fc2534195fa519399b2bd86220366155a4 (patch)
tree3b3f81f07d8b3d96797d52924cd2105bee1255a4
parent77f3fe5709e63c09ed6241e15132a09332d8de53 (diff)
AudioDeviceVolumeManager: set volume per device
Add API in AudioDeviceVolumeManager that allows setting volume for a specific audio device. Bug: 236764441 Test: atest AudioDeviceVolumeManagerTest Change-Id: Iebb277b35dbf5aa49c1fd4a8ec6dd904afa11ad9
-rw-r--r--media/java/android/media/AudioDeviceVolumeManager.java20
-rwxr-xr-xmedia/java/android/media/IAudioService.aidl3
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java91
-rw-r--r--services/core/java/com/android/server/audio/AudioServiceEvents.java39
-rw-r--r--services/core/java/com/android/server/audio/AudioSystemAdapter.java11
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java97
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java5
7 files changed, 253 insertions, 13 deletions
diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java
index fe58cca9395f..c70887672f9e 100644
--- a/media/java/android/media/AudioDeviceVolumeManager.java
+++ b/media/java/android/media/AudioDeviceVolumeManager.java
@@ -61,10 +61,12 @@ public class AudioDeviceVolumeManager {
private static IAudioService sService;
- private final String mPackageName;
+ private final @NonNull String mPackageName;
+ private final @Nullable String mAttributionTag;
public AudioDeviceVolumeManager(Context context) {
mPackageName = context.getApplicationContext().getOpPackageName();
+ mAttributionTag = context.getApplicationContext().getAttributionTag();
}
/**
@@ -287,7 +289,6 @@ public class AudioDeviceVolumeManager {
* @hide
* Removes a previously added listener of changes to device volume behavior.
*/
-
@RequiresPermission(anyOf = {
android.Manifest.permission.MODIFY_AUDIO_ROUTING,
android.Manifest.permission.QUERY_AUDIO_STATE
@@ -299,6 +300,21 @@ public class AudioDeviceVolumeManager {
}
/**
+ * @hide
+ * Sets the volume on the given audio device
+ * @param vi the volume information, only stream-based volumes are supported
+ * @param ada the device for which volume is to be modified
+ */
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada) {
+ try {
+ getService().setDeviceVolume(vi, ada, mPackageName, mAttributionTag);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Return human-readable name for volume behavior
* @param behavior one of the volume behaviors defined in AudioManager
* @return a string for the given behavior
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e28178a8d5d8..90eb9e644f26 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -98,6 +98,9 @@ interface IAudioService {
void setStreamVolumeWithAttribution(int streamType, int index, int flags,
in String callingPackage, in String attributionTag);
+ void setDeviceVolume(in VolumeInfo vi, in AudioDeviceAttributes ada,
+ in String callingPackage, in String attributionTag);
+
oneway void handleVolumeKey(in KeyEvent event, boolean isOnTv,
String callingPackage, String caller);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3a869f859e52..a44a658359ff 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -175,6 +175,7 @@ import com.android.internal.util.Preconditions;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent;
import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
import com.android.server.audio.AudioServiceEvents.VolumeEvent;
import com.android.server.pm.UserManagerInternal;
@@ -3584,7 +3585,8 @@ public class AudioService extends IAudioService.Stub
+ "), do not change associated stream volume");
continue;
}
- setStreamVolume(groupedStream, index, flags, callingPackage, callingPackage,
+ setStreamVolume(groupedStream, index, flags, /*device*/ null,
+ callingPackage, callingPackage,
attributionTag, Binder.getCallingUid(), true /*hasModifyAudioSettings*/);
}
}
@@ -3627,15 +3629,73 @@ public class AudioService extends IAudioService.Stub
return AudioSystem.getMinVolumeIndexForAttributes(attr);
}
+ /** @see AudioDeviceVolumeManager#setDeviceVolume(VolumeInfo, AudioDeviceAttributes)
+ * Part of service interface, check permissions and parameters here */
+ public void setDeviceVolume(@NonNull VolumeInfo vi, @NonNull AudioDeviceAttributes ada,
+ @NonNull String callingPackage, @Nullable String attributionTag) {
+ enforceModifyAudioRoutingPermission();
+ Objects.requireNonNull(vi);
+ Objects.requireNonNull(ada);
+ Objects.requireNonNull(callingPackage);
+ if (!vi.hasStreamType()) {
+ Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception());
+ return;
+ }
+ int index = vi.getVolumeIndex();
+ if (index == VolumeInfo.INDEX_NOT_SET) {
+ throw new IllegalArgumentException("changing device volume requires a volume index");
+ }
+
+ if (vi.getMinVolumeIndex() == VolumeInfo.INDEX_NOT_SET
+ || vi.getMaxVolumeIndex() == VolumeInfo.INDEX_NOT_SET) {
+ // assume index meant to be in stream type range, validate
+ if ((index * 10) < mStreamStates[vi.getStreamType()].getMinIndex()
+ || (index * 10) > mStreamStates[vi.getStreamType()].getMaxIndex()) {
+ throw new IllegalArgumentException("invalid volume index " + index
+ + " not between min/max for stream " + vi.getStreamType());
+ }
+ } else {
+ // check if index needs to be rescaled
+ final int min = (mStreamStates[vi.getStreamType()].getMinIndex() + 5) / 10;
+ final int max = (mStreamStates[vi.getStreamType()].getMaxIndex() + 5) / 10;
+ if (vi.getMinVolumeIndex() != min || vi.getMaxVolumeIndex() != max) {
+ index = rescaleIndex(index,
+ /*srcMin*/ vi.getMinVolumeIndex(), /*srcMax*/ vi.getMaxVolumeIndex(),
+ /*dstMin*/ min, /*dstMax*/ max);
+ }
+ }
+ setStreamVolumeWithAttributionInt(vi.getStreamType(), index, /*flags*/ 0,
+ ada, callingPackage, attributionTag);
+ }
+
/** Retain API for unsupported app usage */
public void setStreamVolume(int streamType, int index, int flags, String callingPackage) {
- setStreamVolumeWithAttribution(streamType, index, flags, callingPackage, null);
+ setStreamVolumeWithAttribution(streamType, index, flags,
+ callingPackage, /*attributionTag*/ null);
}
/** @see AudioManager#setStreamVolume(int, int, int)
* Part of service interface, check permissions here */
public void setStreamVolumeWithAttribution(int streamType, int index, int flags,
String callingPackage, String attributionTag) {
+ setStreamVolumeWithAttributionInt(streamType, index, flags, /*device*/ null,
+ callingPackage, attributionTag);
+ }
+
+ /**
+ * Internal method for a stream type volume change. Can be used to change the volume on a
+ * given device only
+ * @param streamType the stream type whose volume is to be changed
+ * @param index the volume index
+ * @param flags options for volume handling
+ * @param device null when controlling volume for the current routing, otherwise the device
+ * for which volume is being changed
+ * @param callingPackage client side-provided package name of caller, not to be trusted
+ * @param attributionTag client side-provided attribution name, not to be trusted
+ */
+ protected void setStreamVolumeWithAttributionInt(int streamType, int index, int flags,
+ @Nullable AudioDeviceAttributes device,
+ String callingPackage, String attributionTag) {
if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
Log.w(TAG, "Trying to call setStreamVolume() for a11y without"
+ " CHANGE_ACCESSIBILITY_VOLUME callingPackage=" + callingPackage);
@@ -3658,10 +3718,14 @@ public class AudioService extends IAudioService.Stub
return;
}
- sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
- index/*val1*/, flags/*val2*/, callingPackage));
- setStreamVolume(streamType, index, flags, callingPackage, callingPackage,
- attributionTag, Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission());
+ final AudioEventLogger.Event event = (device == null)
+ ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
+ index/*val1*/, flags/*val2*/, callingPackage)
+ : new DeviceVolumeEvent(streamType, index, device, callingPackage);
+ sVolumeLogger.log(event);
+ setStreamVolume(streamType, index, flags, device,
+ callingPackage, callingPackage, attributionTag,
+ Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission());
}
/** @see AudioManager#isUltrasoundSupported() */
@@ -3900,11 +3964,13 @@ public class AudioService extends IAudioService.Stub
}
}
- private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
- String caller, String attributionTag, int uid,
+ private void setStreamVolume(int streamType, int index, int flags,
+ @Nullable AudioDeviceAttributes ada,
+ String callingPackage, String caller, String attributionTag, int uid,
boolean hasModifyAudioSettings) {
if (DEBUG_VOL) {
Log.d(TAG, "setStreamVolume(stream=" + streamType+", index=" + index
+ + ", dev=" + ada
+ ", calling=" + callingPackage + ")");
}
if (mUseFixedVolume) {
@@ -3915,7 +3981,9 @@ public class AudioService extends IAudioService.Stub
int streamTypeAlias = mStreamVolumeAlias[streamType];
VolumeStreamState streamState = mStreamStates[streamTypeAlias];
- final int device = getDeviceForStream(streamType);
+ final int device = (ada == null)
+ ? getDeviceForStream(streamType)
+ : ada.getInternalType();
int oldIndex;
// skip a2dp absolute volume control request when the device
@@ -5419,7 +5487,8 @@ public class AudioService extends IAudioService.Stub
throw new SecurityException("Should only be called from system process");
}
- setStreamVolume(streamType, index, flags, packageName, packageName, null, uid,
+ setStreamVolume(streamType, index, flags, /*device*/ null,
+ packageName, packageName, null, uid,
hasAudioSettingsPermission(uid, pid));
}
@@ -7562,7 +7631,7 @@ public class AudioService extends IAudioService.Stub
&& !isFullyMuted()) {
index = 1;
}
- AudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
+ mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
}
// must be called while synchronized VolumeStreamState.class
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 3225274a8a9b..b5835ce0c847 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -16,7 +16,9 @@
package com.android.server.audio;
+import android.annotation.NonNull;
import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.MediaMetrics;
@@ -145,6 +147,43 @@ public class AudioServiceEvents {
}
}
+ static final class DeviceVolumeEvent extends AudioEventLogger.Event {
+ final int mStream;
+ final int mVolIndex;
+ final String mDeviceNativeType;
+ final String mDeviceAddress;
+ final String mCaller;
+
+ DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
+ String callingPackage) {
+ mStream = streamType;
+ mVolIndex = index;
+ mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType());
+ mDeviceAddress = device.getAddress();
+ mCaller = callingPackage;
+ // log metrics
+ new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT)
+ .set(MediaMetrics.Property.EVENT, "setDeviceVolume")
+ .set(MediaMetrics.Property.STREAM_TYPE,
+ AudioSystem.streamToString(mStream))
+ .set(MediaMetrics.Property.INDEX, mVolIndex)
+ .set(MediaMetrics.Property.DEVICE, mDeviceNativeType)
+ .set(MediaMetrics.Property.ADDRESS, mDeviceAddress)
+ .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
+ .record();
+ }
+
+ @Override
+ public String eventToString() {
+ return new StringBuilder("setDeviceVolume(stream:")
+ .append(AudioSystem.streamToString(mStream))
+ .append(" index:").append(mVolIndex)
+ .append(" device:").append(mDeviceNativeType)
+ .append(" addr:").append(mDeviceAddress)
+ .append(") from ").append(mCaller).toString();
+ }
+ }
+
final static class VolumeEvent extends AudioEventLogger.Event {
static final int VOL_ADJUST_SUGG_VOL = 0;
static final int VOL_ADJUST_STREAM_VOL = 1;
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 758a4ec96288..c3754eb3e44c 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -387,6 +387,17 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
}
/**
+ * Same as {@link AudioSystem#setStreamVolumeIndexAS(int, int, int)}
+ * @param stream
+ * @param index
+ * @param device
+ * @return
+ */
+ public int setStreamVolumeIndexAS(int stream, int index, int device) {
+ return AudioSystem.setStreamVolumeIndexAS(stream, index, device);
+ }
+
+ /**
* Same as {@link AudioSystem#setPhoneState(int, int)}
* @param state
* @param uid
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
new file mode 100644
index 000000000000..7acb6d66c834
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 com.android.server.audio;
+
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.VolumeInfo;
+import android.os.test.TestLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class AudioDeviceVolumeManagerTest {
+ private static final String TAG = "AudioDeviceVolumeManagerTest";
+
+ private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "");
+
+ private Context mContext;
+ private String mPackageName;
+ private AudioSystemAdapter mSpyAudioSystem;
+ private SystemServerAdapter mSystemServer;
+ private SettingsAdapter mSettingsAdapter;
+ private TestLooper mTestLooper;
+
+ private AudioService mAudioService;
+
+
+ @Before
+ public void setUp() throws Exception {
+ mTestLooper = new TestLooper();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mPackageName = mContext.getOpPackageName();
+ mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+
+ mSystemServer = new NoOpSystemServerAdapter();
+ mSettingsAdapter = new NoOpSettingsAdapter();
+ mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
+ mSettingsAdapter, mTestLooper.getLooper()) {
+ @Override
+ public int getDeviceForStream(int stream) {
+ return AudioSystem.DEVICE_OUT_SPEAKER;
+ }
+ };
+
+ mTestLooper.dispatchAll();
+ }
+
+ @Test
+ public void testSetDeviceVolume() {
+ AudioManager am = mContext.getSystemService(AudioManager.class);
+ final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+ final int maxIndex = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+ final int midIndex = (minIndex + maxIndex) / 2;
+ final VolumeInfo volMedia = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+ .setMinVolumeIndex(minIndex)
+ .setMaxVolumeIndex(maxIndex)
+ .build();
+ final VolumeInfo volMin = new VolumeInfo.Builder(volMedia).setVolumeIndex(minIndex).build();
+ final VolumeInfo volMid = new VolumeInfo.Builder(volMedia).setVolumeIndex(midIndex).build();
+ final AudioDeviceAttributes usbDevice = new AudioDeviceAttributes(
+ /*native type*/ AudioSystem.DEVICE_OUT_USB_DEVICE, /*address*/ "bla");
+
+ mAudioService.setDeviceVolume(volMin, usbDevice, mPackageName, TAG);
+ mTestLooper.dispatchAll();
+ verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+ AudioManager.STREAM_MUSIC, minIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
+
+ mAudioService.setDeviceVolume(volMid, usbDevice, mPackageName, TAG);
+ mTestLooper.dispatchAll();
+ verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
+ AudioManager.STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index 09e5d4be14a4..ee9d59b11ed5 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -126,6 +126,11 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter {
}
@Override
+ public int setStreamVolumeIndexAS(int stream, int index, int device) {
+ return AudioSystem.AUDIO_STATUS_OK;
+ }
+
+ @Override
@NonNull
public ArrayList<AudioDeviceAttributes> getDevicesForAttributes(
@NonNull AudioAttributes attributes, boolean forVolume) {