diff options
author | Hongwei Wang <hwwang@google.com> | 2018-02-12 10:29:08 -0800 |
---|---|---|
committer | Hongwei Wang <hwwang@google.com> | 2018-02-16 18:31:20 -0800 |
commit | 33707a9d6e5beb03c5ae010e661035f820cc4bb8 (patch) | |
tree | 0752a73150d6dfbfce5a9eaeaf7fb62bd36d7bbc /service/src/com | |
parent | 3e1b71d5709d3a844bcc2812dd993a6ce3cdd0a0 (diff) | |
download | Car-33707a9d6e5beb03c5ae010e661035f820cc4bb8.tar.gz |
Switches volume control to groupId based
New APIs added
- getVolumeGroupForUsage(int)
- getGroup{Min,Max,}Volume(int)
- setGroupVolume(int)
Old APIs removed
- getUsage{Min,Max,}Volume
- setUsageVolume
Known issue
- No volume sliders in Mojave board since there is no volume group configured
Adds also the validation pass for volume groups configuration
Bug: 72555604
Test: run Car Settings in emulator
Change-Id: I2507a45f2771e26fedc5cfbdb017023fa3d67d46
Diffstat (limited to 'service/src/com')
-rw-r--r-- | service/src/com/android/car/CarAudioDeviceInfo.java (renamed from service/src/com/android/car/AudioDeviceInfoState.java) | 119 | ||||
-rw-r--r-- | service/src/com/android/car/CarAudioService.java | 317 | ||||
-rw-r--r-- | service/src/com/android/car/CarVolumeGroup.java | 162 | ||||
-rw-r--r-- | service/src/com/android/car/CarVolumeGroupsHelper.java | 21 |
4 files changed, 447 insertions, 172 deletions
diff --git a/service/src/com/android/car/AudioDeviceInfoState.java b/service/src/com/android/car/CarAudioDeviceInfo.java index ca380238ed..b8b5e82e54 100644 --- a/service/src/com/android/car/AudioDeviceInfoState.java +++ b/service/src/com/android/car/CarAudioDeviceInfo.java @@ -15,6 +15,7 @@ */ package com.android.car; +import android.annotation.Nullable; import android.car.media.CarAudioManager; import android.content.ContentResolver; import android.content.Context; @@ -22,15 +23,20 @@ import android.media.AudioDeviceInfo; import android.media.AudioDevicePort; import android.media.AudioFormat; import android.media.AudioGain; +import android.media.AudioGainConfig; +import android.media.AudioManager; +import android.media.AudioPort; import android.provider.Settings; +import android.util.Log; import com.android.internal.util.Preconditions; /** - * This class holds the min and max gain index translated from min / max / step values in - * gain control. Also tracks the current gain index on a certain {@link AudioDevicePort}. + * A helper class wraps {@link AudioDeviceInfo}, translates the min / max/ step gain values + * in gain controller to gain index values. */ -/* package */ class AudioDeviceInfoState { +/* package */ class CarAudioDeviceInfo { + private final ContentResolver mContentResolver; private final AudioDeviceInfo mAudioDeviceInfo; private final int mBusNumber; @@ -41,23 +47,22 @@ import com.android.internal.util.Preconditions; private int mCurrentGainIndex; - AudioDeviceInfoState(Context context, AudioDeviceInfo audioDeviceInfo) { + CarAudioDeviceInfo(Context context, AudioDeviceInfo audioDeviceInfo) { mContentResolver = context.getContentResolver(); mAudioDeviceInfo = audioDeviceInfo; mBusNumber = parseDeviceAddress(mAudioDeviceInfo.getAddress()); mSampleRate = getMaxSampleRate(audioDeviceInfo); mChannelCount = getMaxChannels(audioDeviceInfo); final AudioGain audioGain = Preconditions.checkNotNull( - CarAudioService.getAudioGain(audioDeviceInfo.getPort()), - "No audio gain on device port " + audioDeviceInfo); - mMaxGainIndex = CarAudioService.gainToIndex(audioGain, audioGain.maxValue()); - mMinGainIndex = CarAudioService.gainToIndex(audioGain, audioGain.minValue()); + getAudioGain(), "No audio gain on device port " + audioDeviceInfo); + mMaxGainIndex = gainToIndex(audioGain, audioGain.maxValue()); + mMinGainIndex = gainToIndex(audioGain, audioGain.minValue()); // Get the current gain index from persistent storage and fallback to default. mCurrentGainIndex = Settings.Global.getInt(mContentResolver, CarAudioManager.getVolumeSettingsKeyForBus(mBusNumber), -1); if (mCurrentGainIndex < 0) { - mCurrentGainIndex = CarAudioService.gainToIndex(audioGain, audioGain.defaultValue()); + mCurrentGainIndex = gainToIndex(audioGain, audioGain.defaultValue()); } } @@ -65,6 +70,10 @@ import com.android.internal.util.Preconditions; return mAudioDeviceInfo; } + AudioDevicePort getAudioDevicePort() { + return mAudioDeviceInfo.getPort(); + } + int getBusNumber() { return mBusNumber; } @@ -93,9 +102,26 @@ import com.android.internal.util.Preconditions; Preconditions.checkArgument( gainIndex >= mMinGainIndex && gainIndex <= mMaxGainIndex, "Invalid gain index: " + gainIndex); - Settings.Global.putInt(mContentResolver, - CarAudioManager.getVolumeSettingsKeyForBus(mBusNumber), gainIndex); - mCurrentGainIndex = gainIndex; + + // Calls to AudioManager.setAudioPortConfig + AudioGainConfig audioGainConfig = null; + AudioGain audioGain = getAudioGain(); + if (audioGain != null) { + audioGainConfig = getPortGainForIndex(gainIndex); + } + if (audioGainConfig != null) { + int r = AudioManager.setAudioPortGain(getAudioDevicePort(), audioGainConfig); + if (r == 0) { + // Updates the setting and internal state only if setAudioPortGain succeeds + Settings.Global.putInt(mContentResolver, + CarAudioManager.getVolumeSettingsKeyForBus(mBusNumber), gainIndex); + mCurrentGainIndex = gainIndex; + } else { + Log.e(CarLog.TAG_AUDIO, "Failed to setAudioPortGain: " + r); + } + } else { + Log.e(CarLog.TAG_AUDIO, "Failed to construct AudioGainConfig"); + } } /** @@ -149,6 +175,75 @@ import com.android.internal.util.Preconditions; return channels; } + /** + * @return {@link AudioGain} with {@link AudioGain#MODE_JOINT} on a given {@link AudioPort} + */ + AudioGain getAudioGain() { + final AudioDevicePort audioPort = getAudioDevicePort(); + if (audioPort != null && audioPort.gains().length > 0) { + for (AudioGain audioGain : audioPort.gains()) { + if ((audioGain.mode() & AudioGain.MODE_JOINT) != 0) { + return checkAudioGainConfiguration(audioGain); + } + } + } + return null; + } + + /** + * Constraints applied to gain configuration, see also audio_policy_configuration.xml + */ + private AudioGain checkAudioGainConfiguration(AudioGain audioGain) { + Preconditions.checkArgument(audioGain.maxValue() >= audioGain.minValue()); + Preconditions.checkArgument((audioGain.defaultValue() >= audioGain.minValue()) + && (audioGain.defaultValue() <= audioGain.maxValue())); + Preconditions.checkArgument( + ((audioGain.maxValue() - audioGain.minValue()) % audioGain.stepValue()) == 0); + Preconditions.checkArgument( + ((audioGain.defaultValue() - audioGain.minValue()) % audioGain.stepValue()) == 0); + return audioGain; + } + + /** + * @param audioGain {@link AudioGain} on a {@link AudioPort} + * @param gain Gain value in millibel + * @return index value depends on max / min / step of a given {@link AudioGain} + */ + private int gainToIndex(AudioGain audioGain, int gain) { + gain = checkGainBound(audioGain, gain); + return (gain - audioGain.minValue()) / audioGain.stepValue(); + } + + /** + * @param audioGain {@link AudioGain} on a {@link AudioPort} + * @param index index value depends on max / min / step of a given {@link AudioGain} + * @return gain value in millibel + */ + private int indexToGain(AudioGain audioGain, int index) { + final int gain = index * audioGain.stepValue() + audioGain.minValue(); + return checkGainBound(audioGain, gain); + } + + private int checkGainBound(AudioGain audioGain, int gain) { + if (gain < audioGain.minValue() || gain > audioGain.maxValue()) { + throw new RuntimeException("Gain value out of bound: " + gain); + } + return gain; + } + + @Nullable + AudioGainConfig getPortGainForIndex(int index) { + AudioGainConfig audioGainConfig = null; + AudioGain audioGain = getAudioGain(); + if (audioGain != null) { + int gainValue = indexToGain(audioGain, index); + // size of gain values is 1 in MODE_JOINT + audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT, + audioGain.channelMask(), new int[] { gainValue }, 0); + } + return audioGainConfig; + } + @Override public String toString() { return "device: " + mAudioDeviceInfo.getAddress() diff --git a/service/src/com/android/car/CarAudioService.java b/service/src/com/android/car/CarAudioService.java index b1beed920b..20b9f3f3ce 100644 --- a/service/src/com/android/car/CarAudioService.java +++ b/service/src/com/android/car/CarAudioService.java @@ -19,7 +19,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.car.Car; import android.car.media.CarAudioPatchHandle; -import android.car.media.CarVolumeGroup; import android.car.media.ICarAudio; import android.content.Context; import android.content.pm.PackageManager; @@ -30,12 +29,10 @@ import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioDevicePort; import android.media.AudioFormat; -import android.media.AudioGain; import android.media.AudioGainConfig; import android.media.AudioManager; import android.media.AudioPatch; import android.media.AudioPlaybackConfiguration; -import android.media.AudioPort; import android.media.AudioPortConfig; import android.media.audiopolicy.AudioMix; import android.media.audiopolicy.AudioMixingRule; @@ -51,8 +48,11 @@ import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Set; import java.util.stream.Collectors; public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { @@ -103,8 +103,8 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { private final TelephonyManager mTelephonyManager; private final AudioManager mAudioManager; private final boolean mUseDynamicRouting; - private final SparseIntArray mUsageToBus = new SparseIntArray(); - private final SparseArray<AudioDeviceInfoState> mAudioDeviceInfoStates = new SparseArray<>(); + private final SparseIntArray mContextToBus = new SparseIntArray(); + private final SparseArray<CarAudioDeviceInfo> mCarAudioDeviceInfos = new SparseArray<>(); private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback = new AudioPolicy.AudioPolicyVolumeCallback() { @@ -114,17 +114,18 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { Log.v(CarLog.TAG_AUDIO, "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment) + " suggested usage: " + AudioAttributes.usageToString(usage)); - final int currentVolume = getUsageVolume(usage); + final int groupId = getVolumeGroupIdForUsage(usage); + final int currentVolume = getGroupVolume(groupId); final int flags = AudioManager.FLAG_FROM_KEY; switch (adjustment) { case AudioManager.ADJUST_LOWER: - if (currentVolume > getUsageMinVolume(usage)) { - setUsageVolume(usage, currentVolume - 1, flags); + if (currentVolume > getGroupMinVolume(groupId)) { + setGroupVolume(groupId, currentVolume - 1, flags); } break; case AudioManager.ADJUST_RAISE: - if (currentVolume < getUsageMaxVolume(usage)) { - setUsageVolume(usage, currentVolume + 1, flags); + if (currentVolume < getGroupMaxVolume(groupId)) { + setGroupVolume(groupId, currentVolume + 1, flags); } break; case AudioManager.ADJUST_MUTE: @@ -144,7 +145,7 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { }; private AudioPolicy mAudioPolicy; - private CarVolumeGroup[] mCarVolumeGroups = new CarVolumeGroup[0]; + private CarVolumeGroup[] mCarVolumeGroups; public CarAudioService(Context context) { mContext = context; @@ -154,13 +155,19 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { mUseDynamicRouting = res.getBoolean(R.bool.audioUseDynamicRouting); } + /** + * Dynamic routing and volume groups are set only if + * {@link #mUseDynamicRouting} is {@code true}. Otherwise, this service runs in legacy mode. + */ @Override public void init() { if (!mUseDynamicRouting) { Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not configured, run in legacy mode"); return; } + setupDynamicRouting(); + setupVolumeGroups(); } @Override @@ -176,66 +183,61 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { writer.println("*CarAudioService*"); writer.println("mUseDynamicRouting: " + mUseDynamicRouting); if (mUseDynamicRouting) { - int size = mAudioDeviceInfoStates.size(); - for (int i = 0; i < size; i++) { - writer.println("\tBus number: " + mAudioDeviceInfoStates.keyAt(i)); - AudioDeviceInfoState state = mAudioDeviceInfoStates.valueAt(i); - writer.printf("\tGain configuration: %s\n", state.toString()); + for (CarVolumeGroup group : mCarVolumeGroups) { + writer.println("\tVolume group: " + group); } } } /** - * @see {@link android.car.media.CarAudioManager#setUsageVolume(int, int, int)} + * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int)} */ @Override - public void setUsageVolume( - @AudioAttributes.AttributeUsage int usage, int index, int flags) { + public void setGroupVolume(int groupId, int index, int flags) { enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - AudioPort audioPort = getAudioPort(usage); - AudioGainConfig audioGainConfig = null; - AudioGain audioGain = getAudioGain(audioPort); - if (audioGain != null) { - audioGainConfig = getPortGainForIndex(audioPort, index); - } - if (audioGainConfig != null) { - int r = AudioManager.setAudioPortGain(audioPort, audioGainConfig); - if (r == 0) { - AudioDeviceInfoState state = mAudioDeviceInfoStates.get(mUsageToBus.get(usage)); - state.setCurrentGainIndex(gainToIndex(audioGain, audioGainConfig.values()[0])); - } - } + + CarVolumeGroup group = getCarVolumeGroup(groupId); + group.setCurrentGainIndex(index); } /** - * @see {@link android.car.media.CarAudioManager#getUsageMaxVolume(int)} + * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int)} */ @Override - public int getUsageMaxVolume(@AudioAttributes.AttributeUsage int usage) { + public int getGroupMaxVolume(int groupId) { enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - final AudioDeviceInfoState state = mAudioDeviceInfoStates.get(mUsageToBus.get(usage)); - return state == null ? 0 : state.getMaxGainIndex(); + + CarVolumeGroup group = getCarVolumeGroup(groupId); + return group.getMaxGainIndex(); } /** - * TODO(hwwang): some audio usages may have a min volume greater than zero - * @see {@link android.car.media.CarAudioManager#getUsageMinVolume(int)} + * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int)} */ @Override - public int getUsageMinVolume(@AudioAttributes.AttributeUsage int usage) { + public int getGroupMinVolume(int groupId) { enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - final AudioDeviceInfoState state = mAudioDeviceInfoStates.get(mUsageToBus.get(usage)); - return state == null ? 0 : state.getMinGainIndex(); + + CarVolumeGroup group = getCarVolumeGroup(groupId); + return group.getMinGainIndex(); } /** - * @see {@link android.car.media.CarAudioManager#getUsageVolume(int)} + * @see {@link android.car.media.CarAudioManager#getGroupVolume(int)} */ @Override - public int getUsageVolume(@AudioAttributes.AttributeUsage int usage) { + public int getGroupVolume(int groupId) { enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - final AudioDeviceInfoState state = mAudioDeviceInfoStates.get(mUsageToBus.get(usage)); - return state == null ? 0 : state.getCurrentGainIndex(); + + CarVolumeGroup group = getCarVolumeGroup(groupId); + return group.getCurrentGainIndex(); + } + + private CarVolumeGroup getCarVolumeGroup(int groupId) { + Preconditions.checkNotNull(mCarVolumeGroups); + Preconditions.checkArgument(groupId >= 0 && groupId < mCarVolumeGroups.length, + "groupId out of range: " + groupId); + return mCarVolumeGroups[groupId]; } private void setupDynamicRouting() { @@ -245,14 +247,80 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { } AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl); int r = mAudioManager.registerAudioPolicy(audioPolicy); - if (r != 0) { + if (r != AudioManager.SUCCESS) { throw new RuntimeException("registerAudioPolicy failed " + r); } mAudioPolicy = audioPolicy; + } + private void setupVolumeGroups() { + if (mCarAudioDeviceInfos.size() == 0) { + Log.w(CarLog.TAG_AUDIO, "No bus device is configured, skip setupVolumeGroups"); + return; + } final CarVolumeGroupsHelper helper = new CarVolumeGroupsHelper( mContext, R.xml.car_volume_groups); mCarVolumeGroups = helper.loadVolumeGroups(); + for (CarVolumeGroup group : mCarVolumeGroups) { + for (int contextNumber : group.getContexts()) { + int busNumber = mContextToBus.get(contextNumber); + group.bind(contextNumber, busNumber, mCarAudioDeviceInfos.get(busNumber)); + } + Log.v(CarLog.TAG_AUDIO, "Processed volume group: " + group); + } + // Perform validation after all volume groups are processed + if (!validateVolumeGroups()) { + throw new RuntimeException("Invalid volume groups configuration"); + } + } + + /** + * Constraints applied here: + * + * - One context should not appear in two groups + * - All contexts are assigned + * - One bus should not appear in two groups + * - All gain controllers in the same group have same step value + * + * Note that it is fine that there are buses not appear in any group, those buses may be + * reserved for other usages. + * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)} + * + * See also the car_volume_groups.xml configuration + */ + private boolean validateVolumeGroups() { + Set<Integer> contextSet = new HashSet<>(); + Set<Integer> busNumberSet = new HashSet<>(); + for (CarVolumeGroup group : mCarVolumeGroups) { + // One context should not appear in two groups + for (int context : group.getContexts()) { + if (contextSet.contains(context)) { + Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context); + return false; + } + contextSet.add(context); + } + + // One bus should not appear in two groups + for (int busNumber : group.getBusNumbers()) { + if (busNumberSet.contains(busNumber)) { + Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber); + return false; + } + busNumberSet.add(busNumber); + } + } + + // All contexts are assigned + if (contextSet.size() != CONTEXT_NUMBERS.length) { + Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group"); + Log.e(CarLog.TAG_AUDIO, "Assigned contexts " + + Arrays.toString(contextSet.toArray(new Integer[contextSet.size()]))); + Log.e(CarLog.TAG_AUDIO, "All contexts " + Arrays.toString(CONTEXT_NUMBERS)); + return false; + } + + return true; } @Nullable @@ -272,12 +340,12 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { info.toString(), info.getId(), info.getProductName(), info.getAddress(), info.getType())); if (info.getType() == AudioDeviceInfo.TYPE_BUS) { - final AudioDeviceInfoState state = new AudioDeviceInfoState(mContext, info); + final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(mContext, info); // See also the audio_policy_configuration.xml and getBusForContext in // audio control HAL, the bus number should be no less than zero. - if (state.getBusNumber() >= 0) { - mAudioDeviceInfoStates.put(state.getBusNumber(), state); - Log.i(CarLog.TAG_AUDIO, "Valid bus found " + state); + if (carInfo.getBusNumber() >= 0) { + mCarAudioDeviceInfos.put(carInfo.getBusNumber(), carInfo); + Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo); } } } @@ -286,31 +354,31 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { try { for (int contextNumber : CONTEXT_NUMBERS) { int busNumber = audioControl.getBusForContext(contextNumber); - AudioDeviceInfoState state = mAudioDeviceInfoStates.get(busNumber); - if (state == null) { + mContextToBus.put(contextNumber, busNumber); + CarAudioDeviceInfo info = mCarAudioDeviceInfos.get(busNumber); + if (info == null) { Log.w(CarLog.TAG_AUDIO, "No bus configured for context: " + contextNumber); continue; } AudioFormat mixFormat = new AudioFormat.Builder() - .setSampleRate(state.getSampleRate()) + .setSampleRate(info.getSampleRate()) .setEncoding(AudioFormat.ENCODING_PCM_16BIT) - .setChannelMask(state.getChannelCount()) + .setChannelMask(info.getChannelCount()) .build(); - Log.i(CarLog.TAG_AUDIO, String.format( - "Bus number %d, sampleRate:%d, channels:0x%s", - busNumber, state.getSampleRate(), - Integer.toHexString(state.getChannelCount()))); int[] usages = getUsagesForContext(contextNumber); + Log.i(CarLog.TAG_AUDIO, "Bus number: " + busNumber + + " sampleRate: " + info.getSampleRate() + + " channels: " + info.getChannelCount() + + " usages: " + Arrays.toString(usages)); AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder(); for (int usage : usages) { - mUsageToBus.put(usage, busNumber); mixingRuleBuilder.addRule( new AudioAttributes.Builder().setUsage(usage).build(), AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); } AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build()) .setFormat(mixFormat) - .setDevice(state.getAudioDeviceInfo()) + .setDevice(info.getAudioDeviceInfo()) .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) .build(); builder.addMix(audioMix); @@ -399,15 +467,15 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); // Find the named source port - AudioDevicePort sourcePort = null; - AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); - for (AudioDeviceInfo info: devices) { + AudioDeviceInfo sourcePortInfo = null; + AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); + for (AudioDeviceInfo info: deviceInfos) { if (sourceName.equals(info.getProductName())) { // This is the one for which we're looking - sourcePort = info.getPort(); + sourcePortInfo = info; } } - if (sourcePort == null) { + if (sourcePortInfo == null) { throw new IllegalArgumentException("Specified source is not available: " + sourceName); } @@ -421,14 +489,16 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { AudioPortConfig sinkConfig = sinkPort.activeConfig(); // Configure the source port to match the output bus with optional gain adjustment + final CarAudioDeviceInfo helper = new CarAudioDeviceInfo( + mContext, sourcePortInfo); AudioGainConfig audioGainConfig = null; if (gainIndex >= 0) { - audioGainConfig = getPortGainForIndex(sourcePort, gainIndex); + audioGainConfig = helper.getPortGainForIndex(gainIndex); if (audioGainConfig == null) { Log.w(CarLog.TAG_AUDIO, "audio gain could not be applied."); } } - AudioPortConfig sourceConfig = sourcePort.buildConfig( + AudioPortConfig sourceConfig = sourcePortInfo.getPort().buildConfig( sinkConfig.samplingRate(), sinkConfig.channelMask(), sinkConfig.format(), @@ -444,7 +514,7 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { throw new RuntimeException("createAudioPatch failed with code " + result); } if (patch[0] == null) { - throw new RuntimeException("createAudioPatch didn't provide the expected single handle"); + throw new RuntimeException("createAudioPatch didn't provide expected single handle"); } return new CarAudioPatchHandle(patch[0]); @@ -458,7 +528,7 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { // if the client that created a patch quits. // Get the list of active patches - ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); + ArrayList<AudioPatch> patches = new ArrayList<>(); int result = AudioManager.listAudioPatches(patches); if (result != AudioManager.SUCCESS) { throw new RuntimeException("listAudioPatches failed with code " + result); @@ -477,102 +547,53 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { } // If we didn't find a match, then something went awry, but it's probably not fatal... - Log.e(CarLog.TAG_AUDIO, "releaseAudioPatch found no match for " + carPatch.toString()); + Log.e(CarLog.TAG_AUDIO, "releaseAudioPatch found no match for " + carPatch); } @Override - public @NonNull CarVolumeGroup[] getVolumeGroups() { - enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); + public int getVolumeGroupCount() { + enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - return mCarVolumeGroups; + return mCarVolumeGroups == null ? 0 : mCarVolumeGroups.length; } - private void enforcePermission(String permissionName) { - if (mContext.checkCallingOrSelfPermission(permissionName) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException( - "requires permission " + permissionName); - } - } + @Override + public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) { + enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); - /** - * @return {@link AudioDevicePort} that handles the given car audio usage. Multiple car - * audio usages may share one {@link AudioDevicePort} - */ - private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) { - final int busNumber = mUsageToBus.get(usage); - if (mAudioDeviceInfoStates.get(busNumber) != null) { - return mAudioDeviceInfoStates.get(busNumber).getAudioDeviceInfo().getPort(); + if (mCarVolumeGroups == null) { + return -1; } - return null; - } - /** - * @return {@link AudioGain} with {@link AudioGain#MODE_JOINT} on a given {@link AudioPort} - */ - static @Nullable AudioGain getAudioGain(AudioPort audioPort) { - if (audioPort != null && audioPort.gains().length > 0) { - for (AudioGain audioGain : audioPort.gains()) { - if ((audioGain.mode() & AudioGain.MODE_JOINT) != 0) { - return checkAudioGainConfiguration(audioGain); + for (int i = 0; i < mCarVolumeGroups.length; i++) { + int[] contexts = mCarVolumeGroups[i].getContexts(); + for (int context : contexts) { + if (USAGE_TO_CONTEXT.get(usage) == context) { + return i; } } } - return null; - } - - /** - * Constraints applied to gain configuration, see also audio_policy_configuration.xml - */ - private static AudioGain checkAudioGainConfiguration(AudioGain audioGain) { - Preconditions.checkArgument(audioGain.maxValue() >= audioGain.minValue()); - Preconditions.checkArgument((audioGain.defaultValue() >= audioGain.minValue()) - && (audioGain.defaultValue() <= audioGain.maxValue())); - Preconditions.checkArgument( - ((audioGain.maxValue() - audioGain.minValue()) % audioGain.stepValue()) == 0); - Preconditions.checkArgument( - ((audioGain.defaultValue() - audioGain.minValue()) % audioGain.stepValue()) == 0); - return audioGain; + return -1; } - /** - * @param audioGain {@link AudioGain} on a {@link AudioPort} - * @param gain Gain value in millibel - * @return index value depends on max / min / step of a given {@link AudioGain} - */ - static int gainToIndex(AudioGain audioGain, int gain) { - gain = checkGainBound(audioGain, gain); - return (gain - audioGain.minValue()) / audioGain.stepValue(); + private void enforcePermission(String permissionName) { + if (mContext.checkCallingOrSelfPermission(permissionName) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "requires permission " + permissionName); + } } /** - * @param audioGain {@link AudioGain} on a {@link AudioPort} - * @param index index value depends on max / min / step of a given {@link AudioGain} - * @return gain value in millibel + * @return {@link AudioDevicePort} that handles the given car audio usage. + * Multiple usages may share one {@link AudioDevicePort} */ - static int indexToGain(AudioGain audioGain, int index) { - final int gain = index * audioGain.stepValue() + audioGain.minValue(); - return checkGainBound(audioGain, gain); - } - - private static int checkGainBound(AudioGain audioGain, int gain) { - if (gain < audioGain.minValue() || gain > audioGain.maxValue()) { - throw new RuntimeException("Gain value out of bound: " + gain); - } - return gain; - } - - @Nullable - private AudioGainConfig getPortGainForIndex(AudioPort port, int index) { - AudioGainConfig audioGainConfig = null; - AudioGain audioGain = getAudioGain(port); - if (audioGain != null) { - int gainValue = indexToGain(audioGain, index); - // size of gain values is 1 in MODE_JOINT - audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT, - audioGain.channelMask(), new int[] { gainValue }, 0); - } - return audioGainConfig; + private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) { + final int groupId = getVolumeGroupIdForUsage(usage); + final CarVolumeGroup group = Preconditions.checkNotNull(mCarVolumeGroups[groupId], + "Can not find CarVolumeGroup by usage: " + + AudioAttributes.usageToString(usage)); + return group.getAudioDevicePortForContext(USAGE_TO_CONTEXT.get(usage)); } /** @@ -588,7 +609,7 @@ public class CarAudioService extends ICarAudio.Stub implements CarServiceBase { List<AudioPlaybackConfiguration> playbacks = mAudioManager .getActivePlaybackConfigurations() .stream() - .filter(p -> p.isActive()) + .filter(AudioPlaybackConfiguration::isActive) .collect(Collectors.toList()); if (!playbacks.isEmpty()) { // Get audio usage from active playbacks if there is any, last one if multiple diff --git a/service/src/com/android/car/CarVolumeGroup.java b/service/src/com/android/car/CarVolumeGroup.java new file mode 100644 index 0000000000..a0aeff1bd5 --- /dev/null +++ b/service/src/com/android/car/CarVolumeGroup.java @@ -0,0 +1,162 @@ +/* + * 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 com.android.car; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.car.media.CarAudioManager; +import android.content.ContentResolver; +import android.content.Context; +import android.media.AudioDevicePort; +import android.provider.Settings; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.internal.util.Preconditions; + +import java.util.Arrays; + +/** + * A class encapsulates a volume group in car. + * + * Volume in a car is controlled by group. A group holds one or more car audio contexts. + * Call {@link CarAudioManager#getVolumeGroupCount()} to get the count of {@link CarVolumeGroup} + * supported in a car. + */ +/* package */ final class CarVolumeGroup { + + private final ContentResolver mContentResolver; + private final int mId; + private final int[] mContexts; + private final SparseIntArray mContextToBus = new SparseIntArray(); + private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfos = new SparseArray<>(); + + private int mGroupMaxGainIndex = Integer.MIN_VALUE; + private int mGroupMinGainIndex = Integer.MAX_VALUE; + private int mCurrentGainIndex; + + CarVolumeGroup(Context context, int id, @NonNull int[] contexts) { + mContentResolver = context.getContentResolver(); + mId = id; + mContexts = contexts; + + mCurrentGainIndex = Settings.Global.getInt(mContentResolver, + CarAudioManager.getVolumeSettingsKeyForGroup(mId), -1);; + } + + int getId() { + return mId; + } + + int[] getContexts() { + return mContexts; + } + + int[] getBusNumbers() { + final int[] busNumbers = new int[mBusToCarAudioDeviceInfos.size()]; + for (int i = 0; i < busNumbers.length; i++) { + busNumbers[i] = mBusToCarAudioDeviceInfos.keyAt(i); + } + return busNumbers; + } + + /** + * Binds the context number to physical bus number and audio device port information. + * + * @param contextNumber Context number as defined in audio control HAL + * @param busNumber Physical bus number for the audio device port + * @param info {@link CarAudioDeviceInfo} instance relates to the physical bus + */ + void bind(int contextNumber, int busNumber, CarAudioDeviceInfo info) { + if (mBusToCarAudioDeviceInfos.size() > 0) { + final int stepValue = + mBusToCarAudioDeviceInfos.valueAt(0).getAudioGain().stepValue(); + Preconditions.checkArgument( + info.getAudioGain().stepValue() == stepValue, + "Gain controls within one group should have same step value"); + } + mContextToBus.put(contextNumber, busNumber); + mBusToCarAudioDeviceInfos.put(busNumber, info); + if (info.getMaxGainIndex() > mGroupMaxGainIndex) { + mGroupMaxGainIndex = info.getMaxGainIndex(); + } + if (info.getMinGainIndex() < mGroupMinGainIndex) { + mGroupMinGainIndex = info.getMinGainIndex(); + } + if (mCurrentGainIndex < 0) { + // TODO: check the boundary of each gain control. + // It's possible that each bus has different current gain index. If it's due to minimum + // allowed, the current should be set to Math.min(currents), if it's due to maximum + // allowed, the current should be set to Math.max(currents). Otherwise, the current + // should be identical among the buses. + mCurrentGainIndex = info.getCurrentGainIndex(); + } + } + + int getMaxGainIndex() { + return mGroupMaxGainIndex; + } + + int getMinGainIndex() { + return mGroupMinGainIndex; + } + + int getCurrentGainIndex() { + return mCurrentGainIndex; + } + + void setCurrentGainIndex(int gainIndex) { + Preconditions.checkArgument( + gainIndex >= mGroupMinGainIndex && gainIndex <= mGroupMaxGainIndex, + "Invalid gain index: " + gainIndex); + + for (int i = 0; i < mBusToCarAudioDeviceInfos.size(); i++) { + CarAudioDeviceInfo info = mBusToCarAudioDeviceInfos.valueAt(i); + // It's possible that each CarAudioDeviceInfo has different boundary than the group. + if (gainIndex < info.getMinGainIndex()) { + info.setCurrentGainIndex(info.getMinGainIndex()); + } else if (gainIndex > info.getMaxGainIndex()) { + info.setCurrentGainIndex(info.getMaxGainIndex()); + } else { + info.setCurrentGainIndex(gainIndex); + } + } + + mCurrentGainIndex = gainIndex; + Settings.Global.putInt(mContentResolver, + CarAudioManager.getVolumeSettingsKeyForGroup(mId), gainIndex); + } + + @Nullable + AudioDevicePort getAudioDevicePortForContext(int contextNumber) { + final int busNumber = mContextToBus.get(contextNumber, + android.hardware.automotive.audiocontrol.V1_0.ContextNumber.INVALID); + if (busNumber == android.hardware.automotive.audiocontrol.V1_0.ContextNumber.INVALID + || mBusToCarAudioDeviceInfos.get(busNumber) == null) { + return null; + } + return mBusToCarAudioDeviceInfos.get(busNumber).getAudioDevicePort(); + } + + @Override + public String toString() { + return "CarVolumeGroup id: " + mId + + " currentGainIndex: " + mCurrentGainIndex + + " contexts: " + Arrays.toString(mContexts) + + " buses: " + Arrays.toString(getBusNumbers()); + } +} diff --git a/service/src/com/android/car/CarVolumeGroupsHelper.java b/service/src/com/android/car/CarVolumeGroupsHelper.java index 86e0a8944a..af2c6ddb93 100644 --- a/service/src/com/android/car/CarVolumeGroupsHelper.java +++ b/service/src/com/android/car/CarVolumeGroupsHelper.java @@ -16,9 +16,7 @@ package com.android.car; import android.annotation.XmlRes; -import android.car.media.CarVolumeGroup; import android.content.Context; -import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; @@ -37,17 +35,17 @@ import java.util.List; private static final String TAG_GROUP = "group"; private static final String TAG_CONTEXT = "context"; - private final Resources mResources; + private final Context mContext; private final @XmlRes int mXmlConfiguration; CarVolumeGroupsHelper(Context context, @XmlRes int xmlConfiguration) { - mResources = context.getResources(); + mContext = context; mXmlConfiguration = xmlConfiguration; } CarVolumeGroup[] loadVolumeGroups() { List<CarVolumeGroup> carVolumeGroups = new ArrayList<>(); - try (XmlResourceParser parser = mResources.getXml(mXmlConfiguration)) { + try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) { AttributeSet attrs = Xml.asAttributeSet(parser); int type; // Traverse to the first start tag @@ -59,13 +57,15 @@ import java.util.List; throw new RuntimeException("Meta-data does not start with volumeGroups tag"); } int outerDepth = parser.getDepth(); + int id = 0; while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlResourceParser.END_TAG) { continue; } if (TAG_GROUP.equals(parser.getName())) { - carVolumeGroups.add(parseVolumeGroup(attrs, parser)); + carVolumeGroups.add(parseVolumeGroup(id, attrs, parser)); + id++; } } } catch (Exception e) { @@ -74,12 +74,9 @@ import java.util.List; return carVolumeGroups.toArray(new CarVolumeGroup[carVolumeGroups.size()]); } - private CarVolumeGroup parseVolumeGroup(AttributeSet attrs, XmlResourceParser parser) + private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser) throws XmlPullParserException, IOException { int type; - TypedArray a = mResources.obtainAttributes(attrs, R.styleable.volumeGroups_group); - String title = a.getString(R.styleable.volumeGroups_group_name); - a.recycle(); List<Integer> contexts = new ArrayList<>(); int innerDepth = parser.getDepth(); @@ -89,14 +86,14 @@ import java.util.List; continue; } if (TAG_CONTEXT.equals(parser.getName())) { - TypedArray c = mResources.obtainAttributes( + TypedArray c = mContext.getResources().obtainAttributes( attrs, R.styleable.volumeGroups_context); contexts.add(c.getInt(R.styleable.volumeGroups_context_context, -1)); c.recycle(); } } - return new CarVolumeGroup(title, + return new CarVolumeGroup(mContext, id, contexts.stream().mapToInt(i -> i).filter(i -> i >= 0).toArray()); } } |