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
This commit is contained in:
Jean-Michel Trivi
2022-07-13 03:39:51 +00:00
parent 77f3fe5709
commit 118ca2fc25
7 changed files with 253 additions and 13 deletions

View File

@@ -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
@@ -298,6 +299,21 @@ public class AudioDeviceVolumeManager {
"removeOnDeviceVolumeBehaviorChangedListener");
}
/**
* @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

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -386,6 +386,17 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback,
return AudioSystem.isStreamActiveRemotely(stream, inPastMs);
}
/**
* 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

View File

@@ -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);
}
}

View File

@@ -125,6 +125,11 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter {
return mIsStreamActive;
}
@Override
public int setStreamVolumeIndexAS(int stream, int index, int device) {
return AudioSystem.AUDIO_STATUS_OK;
}
@Override
@NonNull
public ArrayList<AudioDeviceAttributes> getDevicesForAttributes(