| /* |
| * Copyright (C) 2015 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.Car; |
| import android.car.media.CarAudioManager; |
| import android.car.media.CarAudioPatchHandle; |
| import android.car.media.ICarAudio; |
| import android.car.media.ICarVolumeCallback; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.hardware.automotive.audiocontrol.V1_0.ContextNumber; |
| import android.hardware.automotive.audiocontrol.V1_0.IAudioControl; |
| 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.AudioPortConfig; |
| import android.media.AudioSystem; |
| import android.media.audiopolicy.AudioMix; |
| import android.media.audiopolicy.AudioMixingRule; |
| import android.media.audiopolicy.AudioPolicy; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.provider.Settings; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.util.SparseIntArray; |
| import android.view.KeyEvent; |
| |
| 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 { |
| |
| private static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA; |
| |
| private static final int[] CONTEXT_NUMBERS = new int[] { |
| ContextNumber.MUSIC, |
| ContextNumber.NAVIGATION, |
| ContextNumber.VOICE_COMMAND, |
| ContextNumber.CALL_RING, |
| ContextNumber.CALL, |
| ContextNumber.ALARM, |
| ContextNumber.NOTIFICATION, |
| ContextNumber.SYSTEM_SOUND |
| }; |
| |
| private static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray(); |
| |
| // For legacy stream type based volume control. |
| // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned. |
| private static final int[] STREAM_TYPES = new int[] { |
| AudioManager.STREAM_MUSIC, |
| AudioManager.STREAM_ALARM, |
| AudioManager.STREAM_RING |
| }; |
| private static final int[] STREAM_TYPE_USAGES = new int[] { |
| AudioAttributes.USAGE_MEDIA, |
| AudioAttributes.USAGE_ALARM, |
| AudioAttributes.USAGE_NOTIFICATION_RINGTONE |
| }; |
| |
| static { |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING, |
| ContextNumber.CALL); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST, |
| ContextNumber.NOTIFICATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT, |
| ContextNumber.NOTIFICATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED, |
| ContextNumber.NOTIFICATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY, |
| ContextNumber.VOICE_COMMAND); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, |
| ContextNumber.NAVIGATION); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION, |
| ContextNumber.SYSTEM_SOUND); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID); |
| USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND); |
| } |
| |
| private final Object mImplLock = new Object(); |
| |
| private final Context mContext; |
| private final TelephonyManager mTelephonyManager; |
| private final AudioManager mAudioManager; |
| private final boolean mUseDynamicRouting; |
| private final boolean mPersistMasterMuteState; |
| private final SparseIntArray mContextToBus = new SparseIntArray(); |
| private final SparseArray<CarAudioDeviceInfo> mCarAudioDeviceInfos = new SparseArray<>(); |
| |
| private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback = |
| new AudioPolicy.AudioPolicyVolumeCallback() { |
| @Override |
| public void onVolumeAdjustment(int adjustment) { |
| final int usage = getSuggestedAudioUsage(); |
| Log.v(CarLog.TAG_AUDIO, |
| "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment) |
| + " suggested usage: " + AudioAttributes.usageToString(usage)); |
| final int groupId = getVolumeGroupIdForUsage(usage); |
| final int currentVolume = getGroupVolume(groupId); |
| final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI; |
| switch (adjustment) { |
| case AudioManager.ADJUST_LOWER: |
| if (currentVolume > getGroupMinVolume(groupId)) { |
| setGroupVolume(groupId, currentVolume - 1, flags); |
| } |
| break; |
| case AudioManager.ADJUST_RAISE: |
| if (currentVolume < getGroupMaxVolume(groupId)) { |
| setGroupVolume(groupId, currentVolume + 1, flags); |
| } |
| break; |
| case AudioManager.ADJUST_MUTE: |
| setMasterMute(true, flags); |
| callbackMasterMuteChange(flags); |
| break; |
| case AudioManager.ADJUST_UNMUTE: |
| setMasterMute(false, flags); |
| callbackMasterMuteChange(flags); |
| break; |
| case AudioManager.ADJUST_TOGGLE_MUTE: |
| setMasterMute(!mAudioManager.isMasterMute(), flags); |
| callbackMasterMuteChange(flags); |
| break; |
| case AudioManager.ADJUST_SAME: |
| default: |
| break; |
| } |
| } |
| }; |
| |
| private final BinderInterfaceContainer<ICarVolumeCallback> mVolumeCallbackContainer = |
| new BinderInterfaceContainer<>(); |
| |
| /** |
| * Simulates {@link ICarVolumeCallback} when it's running in legacy mode. |
| */ |
| private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| switch (intent.getAction()) { |
| case AudioManager.VOLUME_CHANGED_ACTION: |
| int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); |
| int groupId = getVolumeGroupIdForStreamType(streamType); |
| if (groupId == -1) { |
| Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType); |
| } else { |
| callbackGroupVolumeChange(groupId, 0); |
| } |
| break; |
| case AudioManager.MASTER_MUTE_CHANGED_ACTION: |
| callbackMasterMuteChange(0); |
| break; |
| } |
| } |
| }; |
| |
| private AudioPolicy mAudioPolicy; |
| private CarVolumeGroup[] mCarVolumeGroups; |
| |
| public CarAudioService(Context context) { |
| mContext = context; |
| mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); |
| mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting); |
| mPersistMasterMuteState = mContext.getResources().getBoolean( |
| R.bool.audioPersistMasterMuteState); |
| } |
| |
| /** |
| * 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() { |
| synchronized (mImplLock) { |
| if (!mUseDynamicRouting) { |
| Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not configured, run in legacy mode"); |
| setupLegacyVolumeChangedListener(); |
| } else { |
| setupDynamicRouting(); |
| setupVolumeGroups(); |
| } |
| |
| // Restore master mute state if applicable |
| if (mPersistMasterMuteState) { |
| boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(), |
| CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0; |
| setMasterMute(storedMasterMute, 0); |
| } |
| } |
| } |
| |
| @Override |
| public void release() { |
| synchronized (mImplLock) { |
| if (mUseDynamicRouting) { |
| if (mAudioPolicy != null) { |
| mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy); |
| mAudioPolicy = null; |
| } |
| } else { |
| mContext.unregisterReceiver(mLegacyVolumeChangedReceiver); |
| } |
| |
| mVolumeCallbackContainer.clear(); |
| } |
| } |
| |
| @Override |
| public void dump(PrintWriter writer) { |
| writer.println("*CarAudioService*"); |
| writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting)); |
| writer.println("\tPersist master mute state? " + mPersistMasterMuteState); |
| writer.println("\tMaster muted? " + mAudioManager.isMasterMute()); |
| // Empty line for comfortable reading |
| writer.println(); |
| if (mUseDynamicRouting) { |
| for (CarVolumeGroup group : mCarVolumeGroups) { |
| group.dump(writer); |
| } |
| } |
| } |
| |
| /** |
| * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int)} |
| */ |
| @Override |
| public void setGroupVolume(int groupId, int index, int flags) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| callbackGroupVolumeChange(groupId, flags); |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) { |
| mAudioManager.setStreamVolume(STREAM_TYPES[groupId], index, flags); |
| return; |
| } |
| |
| CarVolumeGroup group = getCarVolumeGroup(groupId); |
| group.setCurrentGainIndex(index); |
| } |
| } |
| |
| private void callbackGroupVolumeChange(int groupId, int flags) { |
| for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback : |
| mVolumeCallbackContainer.getInterfaces()) { |
| try { |
| callback.binderInterface.onGroupVolumeChanged(groupId, flags); |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e); |
| } |
| } |
| } |
| |
| private void setMasterMute(boolean mute, int flags) { |
| mAudioManager.setMasterMute(mute, flags); |
| |
| // When the master mute is turned ON, we want the playing app to get a "pause" command. |
| // When the volume is unmuted, we want to resume playback. |
| int keycode = mute ? KeyEvent.KEYCODE_MEDIA_PAUSE : KeyEvent.KEYCODE_MEDIA_PLAY; |
| mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keycode)); |
| mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode)); |
| } |
| |
| private void callbackMasterMuteChange(int flags) { |
| for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback : |
| mVolumeCallbackContainer.getInterfaces()) { |
| try { |
| callback.binderInterface.onMasterMuteChanged(flags); |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e); |
| } |
| } |
| |
| // Persists master mute state if applicable |
| if (mPersistMasterMuteState) { |
| Settings.Global.putInt(mContext.getContentResolver(), |
| CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE, |
| mAudioManager.isMasterMute() ? 1 : 0); |
| } |
| } |
| |
| /** |
| * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int)} |
| */ |
| @Override |
| public int getGroupMaxVolume(int groupId) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) { |
| return mAudioManager.getStreamMaxVolume(STREAM_TYPES[groupId]); |
| } |
| |
| CarVolumeGroup group = getCarVolumeGroup(groupId); |
| return group.getMaxGainIndex(); |
| } |
| } |
| |
| /** |
| * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int)} |
| */ |
| @Override |
| public int getGroupMinVolume(int groupId) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) { |
| return mAudioManager.getStreamMinVolume(STREAM_TYPES[groupId]); |
| } |
| |
| CarVolumeGroup group = getCarVolumeGroup(groupId); |
| return group.getMinGainIndex(); |
| } |
| } |
| |
| /** |
| * @see {@link android.car.media.CarAudioManager#getGroupVolume(int)} |
| */ |
| @Override |
| public int getGroupVolume(int groupId) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) { |
| return mAudioManager.getStreamVolume(STREAM_TYPES[groupId]); |
| } |
| |
| 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 setupLegacyVolumeChangedListener() { |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION); |
| intentFilter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION); |
| mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter); |
| } |
| |
| private void setupDynamicRouting() { |
| final IAudioControl audioControl = getAudioControl(); |
| if (audioControl == null) { |
| return; |
| } |
| AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl); |
| int r = mAudioManager.registerAudioPolicy(audioPolicy); |
| if (r != AudioManager.SUCCESS) { |
| throw new RuntimeException("registerAudioPolicy failed " + r); |
| } |
| mAudioPolicy = audioPolicy; |
| } |
| |
| private void setupVolumeGroups() { |
| Preconditions.checkArgument(mCarAudioDeviceInfos.size() > 0, |
| "No bus device is configured to setup volume groups"); |
| 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)); |
| } |
| |
| // Now that we have all our contexts, ensure the HAL gets our intial value |
| group.setCurrentGainIndex(group.getCurrentGainIndex()); |
| |
| 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 |
| private AudioPolicy getDynamicAudioPolicy(@NonNull IAudioControl audioControl) { |
| AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext); |
| builder.setLooper(Looper.getMainLooper()); |
| |
| // 1st, enumerate all output bus device ports |
| AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); |
| if (deviceInfos.length == 0) { |
| Log.e(CarLog.TAG_AUDIO, "getDynamicAudioPolicy, no output device available, ignore"); |
| return null; |
| } |
| for (AudioDeviceInfo info : deviceInfos) { |
| Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s", |
| info.getId(), info.getAddress(), info.getType())); |
| if (info.getType() == AudioDeviceInfo.TYPE_BUS) { |
| final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info); |
| // See also the audio_policy_configuration.xml and getBusForContext in |
| // audio control HAL, the bus number should be no less than zero. |
| if (carInfo.getBusNumber() >= 0) { |
| mCarAudioDeviceInfos.put(carInfo.getBusNumber(), carInfo); |
| Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo); |
| } |
| } |
| } |
| |
| // 2nd, map context to physical bus |
| try { |
| for (int contextNumber : CONTEXT_NUMBERS) { |
| int busNumber = audioControl.getBusForContext(contextNumber); |
| mContextToBus.put(contextNumber, busNumber); |
| CarAudioDeviceInfo info = mCarAudioDeviceInfos.get(busNumber); |
| if (info == null) { |
| Log.w(CarLog.TAG_AUDIO, "No bus configured for context: " + contextNumber); |
| } |
| } |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "Error mapping context to physical bus", e); |
| } |
| |
| // 3rd, enumerate all physical buses and build the routing policy. |
| // Note that one can not register audio mix for same bus more than once. |
| for (int i = 0; i < mCarAudioDeviceInfos.size(); i++) { |
| int busNumber = mCarAudioDeviceInfos.keyAt(i); |
| boolean hasContext = false; |
| CarAudioDeviceInfo info = mCarAudioDeviceInfos.valueAt(i); |
| AudioFormat mixFormat = new AudioFormat.Builder() |
| .setSampleRate(info.getSampleRate()) |
| .setEncoding(info.getEncodingFormat()) |
| .setChannelMask(info.getChannelCount()) |
| .build(); |
| AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder(); |
| for (int j = 0; j < mContextToBus.size(); j++) { |
| if (mContextToBus.valueAt(j) == busNumber) { |
| hasContext = true; |
| int contextNumber = mContextToBus.keyAt(j); |
| int[] usages = getUsagesForContext(contextNumber); |
| for (int usage : usages) { |
| mixingRuleBuilder.addRule( |
| new AudioAttributes.Builder().setUsage(usage).build(), |
| AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); |
| } |
| Log.i(CarLog.TAG_AUDIO, "Bus number: " + busNumber |
| + " contextNumber: " + contextNumber |
| + " sampleRate: " + info.getSampleRate() |
| + " channels: " + info.getChannelCount() |
| + " usages: " + Arrays.toString(usages)); |
| } |
| } |
| if (hasContext) { |
| // It's a valid case that an audio output bus is defined in |
| // audio_policy_configuration and no context is assigned to it. |
| // In such case, do not build a policy mix with zero rules. |
| AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build()) |
| .setFormat(mixFormat) |
| .setDevice(info.getAudioDeviceInfo()) |
| .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) |
| .build(); |
| builder.addMix(audioMix); |
| } |
| } |
| |
| // 4th, attach the {@link AudioPolicyVolumeCallback} |
| builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback); |
| |
| return builder.build(); |
| } |
| |
| private int[] getUsagesForContext(int contextNumber) { |
| final List<Integer> usages = new ArrayList<>(); |
| for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) { |
| if (USAGE_TO_CONTEXT.valueAt(i) == contextNumber) { |
| usages.add(USAGE_TO_CONTEXT.keyAt(i)); |
| } |
| } |
| return usages.stream().mapToInt(i -> i).toArray(); |
| } |
| |
| @Override |
| public void setFadeTowardFront(float value) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| final IAudioControl audioControlHal = getAudioControl(); |
| if (audioControlHal != null) { |
| try { |
| audioControlHal.setFadeTowardFront(value); |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "setFadeTowardFront failed", e); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setBalanceTowardRight(float value) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| final IAudioControl audioControlHal = getAudioControl(); |
| if (audioControlHal != null) { |
| try { |
| audioControlHal.setBalanceTowardRight(value); |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "setBalanceTowardRight failed", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @return Array of accumulated device addresses, empty array if we found nothing |
| */ |
| @Override |
| public @NonNull String[] getExternalSources() { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); |
| List<String> sourceAddresses = new ArrayList<>(); |
| |
| AudioDeviceInfo[] devices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); |
| if (devices.length == 0) { |
| Log.w(CarLog.TAG_AUDIO, "getExternalSources, no input devices found."); |
| } |
| |
| // Collect the list of non-microphone input ports |
| for (AudioDeviceInfo info : devices) { |
| switch (info.getType()) { |
| // TODO: Can we trim this set down? Especially duplicates like FM vs FM_TUNER? |
| case AudioDeviceInfo.TYPE_FM: |
| case AudioDeviceInfo.TYPE_FM_TUNER: |
| case AudioDeviceInfo.TYPE_TV_TUNER: |
| case AudioDeviceInfo.TYPE_HDMI: |
| case AudioDeviceInfo.TYPE_AUX_LINE: |
| case AudioDeviceInfo.TYPE_LINE_ANALOG: |
| case AudioDeviceInfo.TYPE_LINE_DIGITAL: |
| case AudioDeviceInfo.TYPE_USB_ACCESSORY: |
| case AudioDeviceInfo.TYPE_USB_DEVICE: |
| case AudioDeviceInfo.TYPE_USB_HEADSET: |
| case AudioDeviceInfo.TYPE_IP: |
| case AudioDeviceInfo.TYPE_BUS: |
| String address = info.getAddress(); |
| if (TextUtils.isEmpty(address)) { |
| Log.w(CarLog.TAG_AUDIO, |
| "Discarded device with empty address, type=" + info.getType()); |
| } else { |
| sourceAddresses.add(address); |
| } |
| } |
| } |
| |
| return sourceAddresses.toArray(new String[sourceAddresses.size()]); |
| } |
| } |
| |
| @Override |
| public CarAudioPatchHandle createAudioPatch(String sourceAddress, |
| @AudioAttributes.AttributeUsage int usage, int gainInMillibels) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); |
| return createAudioPatchLocked(sourceAddress, usage, gainInMillibels); |
| } |
| } |
| |
| @Override |
| public void releaseAudioPatch(CarAudioPatchHandle carPatch) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); |
| releaseAudioPatchLocked(carPatch); |
| } |
| } |
| |
| private CarAudioPatchHandle createAudioPatchLocked(String sourceAddress, |
| @AudioAttributes.AttributeUsage int usage, int gainInMillibels) { |
| // Find the named source port |
| AudioDeviceInfo sourcePortInfo = null; |
| AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS); |
| for (AudioDeviceInfo info : deviceInfos) { |
| if (sourceAddress.equals(info.getAddress())) { |
| // This is the one for which we're looking |
| sourcePortInfo = info; |
| break; |
| } |
| } |
| Preconditions.checkNotNull(sourcePortInfo, |
| "Specified source is not available: " + sourceAddress); |
| |
| // Find the output port associated with the given carUsage |
| AudioDevicePort sinkPort = Preconditions.checkNotNull(getAudioPort(usage), |
| "Sink not available for usage: " + AudioAttributes.usageToString(usage)); |
| |
| // {@link android.media.AudioPort#activeConfig()} is valid for mixer port only, |
| // since audio framework has no clue what's active on the device ports. |
| // Therefore we construct an empty / default configuration here, which the audio HAL |
| // implementation should ignore. |
| AudioPortConfig sinkConfig = sinkPort.buildConfig(0, |
| AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null); |
| Log.d(CarLog.TAG_AUDIO, "createAudioPatch sinkConfig: " + sinkConfig); |
| |
| // Configure the source port to match the output port except for a gain adjustment |
| final CarAudioDeviceInfo helper = new CarAudioDeviceInfo(sourcePortInfo); |
| AudioGain audioGain = Preconditions.checkNotNull(helper.getAudioGain(), |
| "Gain controller not available for source port"); |
| |
| // size of gain values is 1 in MODE_JOINT |
| AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT, |
| audioGain.channelMask(), new int[] { gainInMillibels }, 0); |
| // Construct an empty / default configuration excepts gain config here and it's up to the |
| // audio HAL how to interpret this configuration, which the audio HAL |
| // implementation should ignore. |
| AudioPortConfig sourceConfig = sourcePortInfo.getPort().buildConfig(0, |
| AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig); |
| |
| // Create an audioPatch to connect the two ports |
| AudioPatch[] patch = new AudioPatch[] { null }; |
| int result = AudioManager.createAudioPatch(patch, |
| new AudioPortConfig[] { sourceConfig }, |
| new AudioPortConfig[] { sinkConfig }); |
| if (result != AudioManager.SUCCESS) { |
| throw new RuntimeException("createAudioPatch failed with code " + result); |
| } |
| |
| Preconditions.checkNotNull(patch[0], |
| "createAudioPatch didn't provide expected single handle"); |
| Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]); |
| |
| // Ensure the initial volume on output device port |
| int groupId = getVolumeGroupIdForUsage(usage); |
| setGroupVolume(groupId, getGroupVolume(groupId), 0); |
| |
| return new CarAudioPatchHandle(patch[0]); |
| } |
| |
| private void releaseAudioPatchLocked(CarAudioPatchHandle carPatch) { |
| // NOTE: AudioPolicyService::removeNotificationClient will take care of this automatically |
| // if the client that created a patch quits. |
| |
| // FIXME {@link AudioManager#listAudioPatches(ArrayList)} returns old generation of |
| // audio patches after creation |
| ArrayList<AudioPatch> patches = new ArrayList<>(); |
| int result = AudioSystem.listAudioPatches(patches, new int[1]); |
| if (result != AudioManager.SUCCESS) { |
| throw new RuntimeException("listAudioPatches failed with code " + result); |
| } |
| |
| // Look for a patch that matches the provided user side handle |
| for (AudioPatch patch : patches) { |
| if (carPatch.represents(patch)) { |
| // Found it! |
| result = AudioManager.releaseAudioPatch(patch); |
| if (result != AudioManager.SUCCESS) { |
| throw new RuntimeException("releaseAudioPatch failed with code " + result); |
| } |
| return; |
| } |
| } |
| |
| // 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); |
| } |
| |
| @Override |
| public int getVolumeGroupCount() { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) return STREAM_TYPES.length; |
| |
| return mCarVolumeGroups == null ? 0 : mCarVolumeGroups.length; |
| } |
| } |
| |
| @Override |
| public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| if (mCarVolumeGroups == null) { |
| return -1; |
| } |
| |
| 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 -1; |
| } |
| } |
| |
| @Override |
| public @NonNull int[] getUsagesForVolumeGroupId(int groupId) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| // For legacy stream type based volume control |
| if (!mUseDynamicRouting) { |
| return new int[] { STREAM_TYPE_USAGES[groupId] }; |
| } |
| |
| CarVolumeGroup group = getCarVolumeGroup(groupId); |
| Set<Integer> contexts = |
| Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet()); |
| final List<Integer> usages = new ArrayList<>(); |
| for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) { |
| if (contexts.contains(USAGE_TO_CONTEXT.valueAt(i))) { |
| usages.add(USAGE_TO_CONTEXT.keyAt(i)); |
| } |
| } |
| return usages.stream().mapToInt(i -> i).toArray(); |
| } |
| } |
| |
| /** |
| * See {@link android.car.media.CarAudioManager#registerVolumeCallback(IBinder)} |
| */ |
| @Override |
| public void registerVolumeCallback(@NonNull IBinder binder) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| mVolumeCallbackContainer.addBinder(ICarVolumeCallback.Stub.asInterface(binder)); |
| } |
| } |
| |
| /** |
| * See {@link android.car.media.CarAudioManager#unregisterVolumeCallback(IBinder)} |
| */ |
| @Override |
| public void unregisterVolumeCallback(@NonNull IBinder binder) { |
| synchronized (mImplLock) { |
| enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| |
| mVolumeCallbackContainer.removeBinder(ICarVolumeCallback.Stub.asInterface(binder)); |
| } |
| } |
| |
| private void enforcePermission(String permissionName) { |
| if (mContext.checkCallingOrSelfPermission(permissionName) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException("requires permission " + permissionName); |
| } |
| } |
| |
| /** |
| * @return {@link AudioDevicePort} that handles the given car audio usage. |
| * Multiple usages may share one {@link AudioDevicePort} |
| */ |
| 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)); |
| } |
| |
| /** |
| * @return The suggested {@link AudioAttributes} usage to which the volume key events apply |
| */ |
| private @AudioAttributes.AttributeUsage int getSuggestedAudioUsage() { |
| int callState = mTelephonyManager.getCallState(); |
| if (callState == TelephonyManager.CALL_STATE_RINGING) { |
| return AudioAttributes.USAGE_NOTIFICATION_RINGTONE; |
| } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) { |
| return AudioAttributes.USAGE_VOICE_COMMUNICATION; |
| } else { |
| List<AudioPlaybackConfiguration> playbacks = mAudioManager |
| .getActivePlaybackConfigurations() |
| .stream() |
| .filter(AudioPlaybackConfiguration::isActive) |
| .collect(Collectors.toList()); |
| if (!playbacks.isEmpty()) { |
| // Get audio usage from active playbacks if there is any, last one if multiple |
| return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage(); |
| } else { |
| // TODO(b/72695246): Otherwise, get audio usage from foreground activity/window |
| return DEFAULT_AUDIO_USAGE; |
| } |
| } |
| } |
| |
| /** |
| * Gets volume group by a given legacy stream type |
| * @param streamType Legacy stream type such as {@link AudioManager#STREAM_MUSIC} |
| * @return volume group id mapped from stream type |
| */ |
| private int getVolumeGroupIdForStreamType(int streamType) { |
| int groupId = -1; |
| for (int i = 0; i < STREAM_TYPES.length; i++) { |
| if (streamType == STREAM_TYPES[i]) { |
| groupId = i; |
| break; |
| } |
| } |
| return groupId; |
| } |
| |
| @Nullable |
| private static IAudioControl getAudioControl() { |
| try { |
| return IAudioControl.getService(); |
| } catch (RemoteException e) { |
| Log.e(CarLog.TAG_AUDIO, "Failed to get IAudioControl service", e); |
| } catch (NoSuchElementException e) { |
| Log.e(CarLog.TAG_AUDIO, "IAudioControl service not registered yet"); |
| } |
| return null; |
| } |
| } |