| /* |
| * 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.car.Car; |
| import android.car.VehicleZoneUtil; |
| import android.car.media.CarAudioManager; |
| import android.car.media.CarAudioManager.OnParameterChangeListener; |
| import android.car.media.ICarAudio; |
| import android.car.media.ICarAudioCallback; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.media.AudioAttributes; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioFocusInfo; |
| import android.media.AudioFormat; |
| import android.media.AudioManager; |
| import android.media.IVolumeController; |
| import android.media.audiopolicy.AudioMix; |
| import android.media.audiopolicy.AudioMixingRule; |
| import android.media.audiopolicy.AudioPolicy; |
| import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import com.android.car.hal.AudioHalService; |
| import com.android.car.hal.AudioHalService.AudioHalFocusListener; |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| public class CarAudioService extends ICarAudio.Stub implements CarServiceBase, |
| AudioHalFocusListener, OnParameterChangeListener { |
| |
| public interface AudioContextChangeListener { |
| /** |
| * Notifies the current primary audio context (app holding focus). |
| * If there is no active context, context will be 0. |
| * Will use context like CarAudioManager.CAR_AUDIO_USAGE_* |
| */ |
| void onContextChange(int primaryFocusContext, int primaryFocusPhysicalStream); |
| } |
| |
| private final long mFocusResponseWaitTimeoutMs; |
| |
| private final int mNumConsecutiveHalFailuresForCanError; |
| |
| private static final String TAG_FOCUS = CarLog.TAG_AUDIO + ".FOCUS"; |
| |
| private static final boolean DBG = false; |
| private static final boolean DBG_DYNAMIC_AUDIO_ROUTING = false; |
| |
| /** |
| * For no focus play case, wait this much to send focus request. This ugly time is necessary |
| * as focus could have been already requested by app but the event is not delivered to car |
| * service yet. In such case, requesting focus in advance can lead into request with wrong |
| * context. So let it wait for this much to make sure that focus change is delivered. |
| */ |
| private static final long NO_FOCUS_PLAY_WAIT_TIME_MS = 100; |
| |
| private final AudioHalService mAudioHal; |
| private final Context mContext; |
| private final HandlerThread mFocusHandlerThread; |
| private final CarAudioFocusChangeHandler mFocusHandler; |
| private final SystemFocusListener mSystemFocusListener; |
| private final CarVolumeService mVolumeService; |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private AudioPolicy mAudioPolicy; |
| @GuardedBy("mLock") |
| private FocusState mCurrentFocusState = FocusState.STATE_LOSS; |
| /** Focus state received, but not handled yet. Once handled, this will be set to null. */ |
| @GuardedBy("mLock") |
| private FocusState mFocusReceived = null; |
| @GuardedBy("mLock") |
| private FocusRequest mLastFocusRequestToCar = null; |
| @GuardedBy("mLock") |
| private LinkedList<AudioFocusInfo> mPendingFocusChanges = new LinkedList<>(); |
| @GuardedBy("mLock") |
| private AudioFocusInfo mTopFocusInfo = null; |
| /** previous top which may be in ducking state */ |
| @GuardedBy("mLock") |
| private AudioFocusInfo mSecondFocusInfo = null; |
| |
| private AudioRoutingPolicy mAudioRoutingPolicy; |
| private final AudioManager mAudioManager; |
| private final CanBusErrorNotifier mCanBusErrorNotifier; |
| private final BottomAudioFocusListener mBottomAudioFocusListener = |
| new BottomAudioFocusListener(); |
| private final CarProxyAndroidFocusListener mCarProxyAudioFocusListener = |
| new CarProxyAndroidFocusListener(); |
| private final MediaMuteAudioFocusListener mMediaMuteAudioFocusListener = |
| new MediaMuteAudioFocusListener(); |
| |
| @GuardedBy("mLock") |
| private int mBottomFocusState; |
| @GuardedBy("mLock") |
| private boolean mRadioOrExtSourceActive = false; |
| @GuardedBy("mLock") |
| private boolean mCallActive = false; |
| @GuardedBy("mLock") |
| private int mCurrentAudioContexts = 0; |
| @GuardedBy("mLock") |
| private int mCurrentPrimaryAudioContext = 0; |
| @GuardedBy("mLock") |
| private int mCurrentPrimaryPhysicalStream = 0; |
| @GuardedBy("mLock") |
| private AudioContextChangeListener mAudioContextChangeListener; |
| @GuardedBy("mLock") |
| private CarAudioContextChangeHandler mCarAudioContextChangeHandler; |
| @GuardedBy("mLock") |
| private boolean mIsRadioExternal; |
| @GuardedBy("mLock") |
| private int mNumConsecutiveHalFailures; |
| |
| @GuardedBy("mLock") |
| private boolean mExternalRoutingHintSupported; |
| @GuardedBy("mLock") |
| private Map<String, AudioHalService.ExtRoutingSourceInfo> mExternalRoutingTypes; |
| @GuardedBy("mLock") |
| private Set<String> mExternalRadioRoutingTypes; |
| @GuardedBy("mLock") |
| private String mDefaultRadioRoutingType; |
| @GuardedBy("mLock") |
| private Set<String> mExternalNonRadioRoutingTypes; |
| @GuardedBy("mLock") |
| private int mRadioPhysicalStream; |
| @GuardedBy("mLock") |
| private int[] mExternalRoutings = {0, 0, 0, 0}; |
| private int[] mExternalRoutingsScratch = {0, 0, 0, 0}; |
| private final int[] mExternalRoutingsForFocusRelease = {0, 0, 0, 0}; |
| private final ExtSourceInfo mExtSourceInfoScratch = new ExtSourceInfo(); |
| @GuardedBy("mLock") |
| private int mSystemSoundPhysicalStream; |
| @GuardedBy("mLock") |
| private boolean mSystemSoundPhysicalStreamActive; |
| |
| private final boolean mUseDynamicRouting; |
| |
| private final AudioAttributes mAttributeBottom = |
| CarAudioAttributesUtil.getAudioAttributesForCarUsage( |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_BOTTOM); |
| private final AudioAttributes mAttributeCarExternal = |
| CarAudioAttributesUtil.getAudioAttributesForCarUsage( |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY); |
| |
| @GuardedBy("mLock") |
| private final BinderInterfaceContainer<ICarAudioCallback> mAudioParamListeners = |
| new BinderInterfaceContainer<>(); |
| @GuardedBy("mLock") |
| private HashSet<String> mAudioParamKeys; |
| |
| public CarAudioService(Context context, AudioHalService audioHal, |
| CarInputService inputService, CanBusErrorNotifier errorNotifier) { |
| mAudioHal = audioHal; |
| mContext = context; |
| mFocusHandlerThread = new HandlerThread(CarLog.TAG_AUDIO); |
| mSystemFocusListener = new SystemFocusListener(); |
| mFocusHandlerThread.start(); |
| mFocusHandler = new CarAudioFocusChangeHandler(mFocusHandlerThread.getLooper()); |
| mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); |
| mCanBusErrorNotifier = errorNotifier; |
| Resources res = context.getResources(); |
| mFocusResponseWaitTimeoutMs = (long) res.getInteger(R.integer.audioFocusWaitTimeoutMs); |
| mNumConsecutiveHalFailuresForCanError = |
| (int) res.getInteger(R.integer.consecutiveHalFailures); |
| mUseDynamicRouting = res.getBoolean(R.bool.audioUseDynamicRouting); |
| mVolumeService = new CarVolumeService(mContext, this, mAudioHal, inputService); |
| } |
| |
| @Override |
| public AudioAttributes getAudioAttributesForCarUsage(int carUsage) { |
| return CarAudioAttributesUtil.getAudioAttributesForCarUsage(carUsage); |
| } |
| |
| @Override |
| public void init() { |
| AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext); |
| builder.setLooper(Looper.getMainLooper()); |
| boolean isFocusSupported = mAudioHal.isFocusSupported(); |
| if (isFocusSupported) { |
| builder.setAudioPolicyFocusListener(mSystemFocusListener); |
| FocusState currentState = FocusState.create(mAudioHal.getCurrentFocusState()); |
| int r = mAudioManager.requestAudioFocus(mBottomAudioFocusListener, mAttributeBottom, |
| AudioManager.AUDIOFOCUS_GAIN, AudioManager.AUDIOFOCUS_FLAG_DELAY_OK); |
| synchronized (mLock) { |
| if (r == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| mBottomFocusState = AudioManager.AUDIOFOCUS_GAIN; |
| } else { |
| mBottomFocusState = AudioManager.AUDIOFOCUS_LOSS_TRANSIENT; |
| } |
| mCurrentFocusState = currentState; |
| mCurrentAudioContexts = 0; |
| } |
| } |
| int audioHwVariant = mAudioHal.getHwVariant(); |
| AudioRoutingPolicy audioRoutingPolicy = AudioRoutingPolicy.create(mContext, audioHwVariant); |
| if (mUseDynamicRouting) { |
| setupDynamicRouting(audioRoutingPolicy, builder); |
| } |
| AudioPolicy audioPolicy = null; |
| if (isFocusSupported || mUseDynamicRouting) { |
| audioPolicy = builder.build(); |
| } |
| mAudioHal.setFocusListener(this); |
| mAudioHal.setAudioRoutingPolicy(audioRoutingPolicy); |
| mAudioHal.setOnParameterChangeListener(this); |
| // get call outside lock as it can take time |
| HashSet<String> externalRadioRoutingTypes = new HashSet<>(); |
| HashSet<String> externalNonRadioRoutingTypes = new HashSet<>(); |
| Map<String, AudioHalService.ExtRoutingSourceInfo> externalRoutingTypes = |
| mAudioHal.getExternalAudioRoutingTypes(); |
| if (externalRoutingTypes != null) { |
| for (String routingType : externalRoutingTypes.keySet()) { |
| if (routingType.startsWith("RADIO_")) { |
| externalRadioRoutingTypes.add(routingType); |
| } else { |
| externalNonRadioRoutingTypes.add(routingType); |
| } |
| } |
| } |
| // select default radio routing. AM_FM -> AM_FM_HD -> whatever with AM or FM -> first one |
| String defaultRadioRouting = null; |
| if (externalRadioRoutingTypes.contains(CarAudioManager.CAR_RADIO_TYPE_AM_FM)) { |
| defaultRadioRouting = CarAudioManager.CAR_RADIO_TYPE_AM_FM; |
| } else if (externalRadioRoutingTypes.contains(CarAudioManager.CAR_RADIO_TYPE_AM_FM_HD)) { |
| defaultRadioRouting = CarAudioManager.CAR_RADIO_TYPE_AM_FM_HD; |
| } else { |
| for (String radioType : externalRadioRoutingTypes) { |
| // set to 1st one |
| if (defaultRadioRouting == null) { |
| defaultRadioRouting = radioType; |
| } |
| if (radioType.contains("AM") || radioType.contains("FM")) { |
| defaultRadioRouting = radioType; |
| break; |
| } |
| } |
| } |
| if (defaultRadioRouting == null) { // no radio type defined. fall back to AM_FM |
| defaultRadioRouting = CarAudioManager.CAR_RADIO_TYPE_AM_FM; |
| } |
| synchronized (mLock) { |
| if (audioPolicy != null) { |
| mAudioPolicy = audioPolicy; |
| } |
| mRadioPhysicalStream = audioRoutingPolicy.getPhysicalStreamForLogicalStream( |
| CarAudioManager.CAR_AUDIO_USAGE_RADIO);; |
| mSystemSoundPhysicalStream = audioRoutingPolicy.getPhysicalStreamForLogicalStream( |
| CarAudioManager.CAR_AUDIO_USAGE_SYSTEM_SOUND); |
| mSystemSoundPhysicalStreamActive = false; |
| mAudioRoutingPolicy = audioRoutingPolicy; |
| mIsRadioExternal = mAudioHal.isRadioExternal(); |
| if (externalRoutingTypes != null) { |
| mExternalRoutingHintSupported = true; |
| mExternalRoutingTypes = externalRoutingTypes; |
| } else { |
| mExternalRoutingHintSupported = false; |
| mExternalRoutingTypes = new HashMap<>(); |
| } |
| mExternalRadioRoutingTypes = externalRadioRoutingTypes; |
| mExternalNonRadioRoutingTypes = externalNonRadioRoutingTypes; |
| mDefaultRadioRoutingType = defaultRadioRouting; |
| Arrays.fill(mExternalRoutings, 0); |
| populateParameterKeysLocked(); |
| } |
| mVolumeService.init(); |
| |
| // Register audio policy only after this class is fully initialized. |
| int r = mAudioManager.registerAudioPolicy(audioPolicy); |
| if (r != 0) { |
| throw new RuntimeException("registerAudioPolicy failed " + r); |
| } |
| } |
| |
| private void setupDynamicRouting(AudioRoutingPolicy audioRoutingPolicy, |
| AudioPolicy.Builder audioPolicyBuilder) { |
| AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); |
| if (deviceInfos.length == 0) { |
| Log.e(CarLog.TAG_AUDIO, "setupDynamicRouting, no output device available, ignore"); |
| return; |
| } |
| int numPhysicalStreams = audioRoutingPolicy.getPhysicalStreamsCount(); |
| AudioDeviceInfo[] devicesToRoute = new AudioDeviceInfo[numPhysicalStreams]; |
| for (AudioDeviceInfo info : deviceInfos) { |
| if (DBG_DYNAMIC_AUDIO_ROUTING) { |
| Log.v(CarLog.TAG_AUDIO, String.format( |
| "output device=%s id=%d name=%s addr=%s type=%s", |
| info.toString(), info.getId(), info.getProductName(), info.getAddress(), |
| info.getType())); |
| } |
| if (info.getType() == AudioDeviceInfo.TYPE_BUS) { |
| int addressNumeric = parseDeviceAddress(info.getAddress()); |
| if (addressNumeric >= 0 && addressNumeric < numPhysicalStreams) { |
| devicesToRoute[addressNumeric] = info; |
| Log.i(CarLog.TAG_AUDIO, String.format( |
| "valid bus found, devie=%s id=%d name=%s addr=%s", |
| info.toString(), info.getId(), info.getProductName(), info.getAddress()) |
| ); |
| } |
| } |
| } |
| for (int i = 0; i < numPhysicalStreams; i++) { |
| AudioDeviceInfo info = devicesToRoute[i]; |
| if (info == null) { |
| Log.e(CarLog.TAG_AUDIO, "setupDynamicRouting, cannot find device for address " + i); |
| return; |
| } |
| int sampleRate = getMaxSampleRate(info); |
| int channels = getMaxChannles(info); |
| AudioFormat mixFormat = new AudioFormat.Builder() |
| .setSampleRate(sampleRate) |
| .setEncoding(AudioFormat.ENCODING_PCM_16BIT) |
| .setChannelMask(channels) |
| .build(); |
| Log.i(CarLog.TAG_AUDIO, String.format( |
| "Physical stream %d, sampleRate:%d, channles:0x%s", i, sampleRate, |
| Integer.toHexString(channels))); |
| int[] logicalStreams = audioRoutingPolicy.getLogicalStreamsForPhysicalStream(i); |
| AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder(); |
| for (int logicalStream : logicalStreams) { |
| mixingRuleBuilder.addRule( |
| CarAudioAttributesUtil.getAudioAttributesForCarUsage(logicalStream), |
| AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); |
| } |
| AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build()) |
| .setFormat(mixFormat) |
| .setDevice(info) |
| .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER) |
| .build(); |
| audioPolicyBuilder.addMix(audioMix); |
| } |
| } |
| |
| /** |
| * Parse device address. Expected format is BUS%d_%s, address, usage hint |
| * @return valid address (from 0 to positive) or -1 for invalid address. |
| */ |
| private int parseDeviceAddress(String address) { |
| String[] words = address.split("_"); |
| int addressParsed = -1; |
| if (words[0].startsWith("BUS")) { |
| try { |
| addressParsed = Integer.parseInt(words[0].substring(3)); |
| } catch (NumberFormatException e) { |
| //ignore |
| } |
| } |
| if (addressParsed < 0) { |
| return -1; |
| } |
| return addressParsed; |
| } |
| |
| private int getMaxSampleRate(AudioDeviceInfo info) { |
| int[] sampleRates = info.getSampleRates(); |
| if (sampleRates == null || sampleRates.length == 0) { |
| return 48000; |
| } |
| int sampleRate = sampleRates[0]; |
| for (int i = 1; i < sampleRates.length; i++) { |
| if (sampleRates[i] > sampleRate) { |
| sampleRate = sampleRates[i]; |
| } |
| } |
| return sampleRate; |
| } |
| |
| private int getMaxChannles(AudioDeviceInfo info) { |
| int[] channelMasks = info.getChannelMasks(); |
| if (channelMasks == null) { |
| return AudioFormat.CHANNEL_OUT_STEREO; |
| } |
| int channels = AudioFormat.CHANNEL_OUT_MONO; |
| int numChannels = 1; |
| for (int i = 0; i < channelMasks.length; i++) { |
| int currentNumChannles = VehicleZoneUtil.getNumberOfZones(channelMasks[i]); |
| if (currentNumChannles > numChannels) { |
| numChannels = currentNumChannles; |
| channels = channelMasks[i]; |
| } |
| } |
| return channels; |
| } |
| |
| @Override |
| public void release() { |
| mFocusHandler.cancelAll(); |
| mAudioManager.abandonAudioFocus(mBottomAudioFocusListener); |
| mAudioManager.abandonAudioFocus(mCarProxyAudioFocusListener); |
| AudioPolicy audioPolicy; |
| synchronized (mLock) { |
| mAudioParamKeys = null; |
| mCurrentFocusState = FocusState.STATE_LOSS; |
| mLastFocusRequestToCar = null; |
| mTopFocusInfo = null; |
| mPendingFocusChanges.clear(); |
| mRadioOrExtSourceActive = false; |
| if (mCarAudioContextChangeHandler != null) { |
| mCarAudioContextChangeHandler.cancelAll(); |
| mCarAudioContextChangeHandler = null; |
| } |
| mAudioContextChangeListener = null; |
| mCurrentPrimaryAudioContext = 0; |
| audioPolicy = mAudioPolicy; |
| mAudioPolicy = null; |
| mExternalRoutingTypes.clear(); |
| mExternalRadioRoutingTypes.clear(); |
| mExternalNonRadioRoutingTypes.clear(); |
| } |
| if (audioPolicy != null) { |
| mAudioManager.unregisterAudioPolicyAsync(audioPolicy); |
| } |
| } |
| |
| public synchronized void setAudioContextChangeListener(Looper looper, |
| AudioContextChangeListener listener) { |
| if (looper == null || listener == null) { |
| throw new IllegalArgumentException("looper or listener null"); |
| } |
| if (mCarAudioContextChangeHandler != null) { |
| mCarAudioContextChangeHandler.cancelAll(); |
| } |
| mCarAudioContextChangeHandler = new CarAudioContextChangeHandler(looper); |
| mAudioContextChangeListener = listener; |
| } |
| |
| @Override |
| public void dump(PrintWriter writer) { |
| synchronized (mLock) { |
| writer.println("*CarAudioService*"); |
| writer.println(" mCurrentFocusState:" + mCurrentFocusState + |
| " mLastFocusRequestToCar:" + mLastFocusRequestToCar); |
| writer.println(" mCurrentAudioContexts:0x" + |
| Integer.toHexString(mCurrentAudioContexts)); |
| writer.println(" mCallActive:" + mCallActive + " mRadioOrExtSourceActive:" + |
| mRadioOrExtSourceActive); |
| writer.println(" mCurrentPrimaryAudioContext:" + mCurrentPrimaryAudioContext + |
| " mCurrentPrimaryPhysicalStream:" + mCurrentPrimaryPhysicalStream); |
| writer.println(" mIsRadioExternal:" + mIsRadioExternal); |
| writer.println(" mNumConsecutiveHalFailures:" + mNumConsecutiveHalFailures); |
| writer.println(" media muted:" + mMediaMuteAudioFocusListener.isMuted()); |
| writer.println(" mAudioPolicy:" + mAudioPolicy); |
| mAudioRoutingPolicy.dump(writer); |
| writer.println(" mExternalRoutingHintSupported:" + mExternalRoutingHintSupported); |
| if (mExternalRoutingHintSupported) { |
| writer.println(" mDefaultRadioRoutingType:" + mDefaultRadioRoutingType); |
| writer.println(" Routing Types:"); |
| for (Entry<String, AudioHalService.ExtRoutingSourceInfo> entry : |
| mExternalRoutingTypes.entrySet()) { |
| writer.println(" type:" + entry.getKey() + " info:" + entry.getValue()); |
| } |
| } |
| if (mAudioParamKeys != null) { |
| writer.println("** Audio parameter keys**"); |
| for (String key : mAudioParamKeys) { |
| writer.println(" " + key); |
| } |
| } |
| } |
| writer.println("** Dump CarVolumeService**"); |
| mVolumeService.dump(writer); |
| } |
| |
| @Override |
| public void onFocusChange(int focusState, int streams, int externalFocus) { |
| synchronized (mLock) { |
| mFocusReceived = FocusState.create(focusState, streams, externalFocus); |
| // wake up thread waiting for focus response. |
| mLock.notifyAll(); |
| } |
| mFocusHandler.handleFocusChange(); |
| } |
| |
| @Override |
| public void onStreamStatusChange(int streamNumber, boolean streamActive) { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "onStreamStatusChange stream:" + streamNumber + ", active:" + |
| streamActive); |
| } |
| mFocusHandler.handleStreamStateChange(streamNumber, streamActive); |
| } |
| |
| @Override |
| public void setStreamVolume(int streamType, int index, int flags) { |
| enforceAudioVolumePermission(); |
| mVolumeService.setStreamVolume(streamType, index, flags); |
| } |
| |
| @Override |
| public void setVolumeController(IVolumeController controller) { |
| enforceAudioVolumePermission(); |
| mVolumeService.setVolumeController(controller); |
| } |
| |
| @Override |
| public int getStreamMaxVolume(int streamType) { |
| enforceAudioVolumePermission(); |
| return mVolumeService.getStreamMaxVolume(streamType); |
| } |
| |
| @Override |
| public int getStreamMinVolume(int streamType) { |
| enforceAudioVolumePermission(); |
| return mVolumeService.getStreamMinVolume(streamType); |
| } |
| |
| @Override |
| public int getStreamVolume(int streamType) { |
| enforceAudioVolumePermission(); |
| return mVolumeService.getStreamVolume(streamType); |
| } |
| |
| @Override |
| public boolean isMediaMuted() { |
| return mMediaMuteAudioFocusListener.isMuted(); |
| } |
| |
| @Override |
| public boolean setMediaMute(boolean mute) { |
| enforceAudioVolumePermission(); |
| boolean currentState = isMediaMuted(); |
| if (mute == currentState) { |
| return currentState; |
| } |
| if (mute) { |
| return mMediaMuteAudioFocusListener.mute(); |
| } else { |
| return mMediaMuteAudioFocusListener.unMute(); |
| } |
| } |
| |
| @Override |
| public AudioAttributes getAudioAttributesForRadio(String radioType) { |
| synchronized (mLock) { |
| if (!mExternalRadioRoutingTypes.contains(radioType)) { // type not exist |
| throw new IllegalArgumentException("Specified radio type is not available:" + |
| radioType); |
| } |
| } |
| return CarAudioAttributesUtil.getCarRadioAttributes(radioType); |
| } |
| |
| @Override |
| public AudioAttributes getAudioAttributesForExternalSource(String externalSourceType) { |
| synchronized (mLock) { |
| if (!mExternalNonRadioRoutingTypes.contains(externalSourceType)) { // type not exist |
| throw new IllegalArgumentException("Specified ext source type is not available:" + |
| externalSourceType); |
| } |
| } |
| return CarAudioAttributesUtil.getCarExtSourceAttributes(externalSourceType); |
| } |
| |
| @Override |
| public String[] getSupportedExternalSourceTypes() { |
| synchronized (mLock) { |
| return mExternalNonRadioRoutingTypes.toArray( |
| new String[mExternalNonRadioRoutingTypes.size()]); |
| } |
| } |
| |
| @Override |
| public String[] getSupportedRadioTypes() { |
| synchronized (mLock) { |
| return mExternalRadioRoutingTypes.toArray( |
| new String[mExternalRadioRoutingTypes.size()]); |
| } |
| } |
| |
| @Override |
| public void onParameterChange(String parameters) { |
| for (BinderInterfaceContainer.BinderInterface<ICarAudioCallback> client : |
| mAudioParamListeners.getInterfaces()) { |
| try { |
| client.binderInterface.onParameterChange(parameters); |
| } catch (RemoteException e) { |
| // ignore. death handler will handle it. |
| } |
| } |
| } |
| |
| @Override |
| public String[] getParameterKeys() { |
| enforceAudioSettingsPermission(); |
| return mAudioHal.getAudioParameterKeys(); |
| } |
| |
| @Override |
| public void setParameters(String parameters) { |
| enforceAudioSettingsPermission(); |
| if (parameters == null) { |
| throw new IllegalArgumentException("null parameters"); |
| } |
| String[] keyValues = parameters.split(";"); |
| synchronized (mLock) { |
| for (String keyValue : keyValues) { |
| String[] keyValuePair = keyValue.split("="); |
| if (keyValuePair.length != 2) { |
| throw new IllegalArgumentException("Wrong audio parameter:" + parameters); |
| } |
| assertPamameterKeysLocked(keyValuePair[0]); |
| } |
| } |
| mAudioHal.setAudioParameters(parameters); |
| } |
| |
| @Override |
| public String getParameters(String keys) { |
| enforceAudioSettingsPermission(); |
| if (keys == null) { |
| throw new IllegalArgumentException("null keys"); |
| } |
| synchronized (mLock) { |
| for (String key : keys.split(";")) { |
| assertPamameterKeysLocked(key); |
| } |
| } |
| return mAudioHal.getAudioParameters(keys); |
| } |
| |
| @Override |
| public void registerOnParameterChangeListener(ICarAudioCallback callback) { |
| enforceAudioSettingsPermission(); |
| if (callback == null) { |
| throw new IllegalArgumentException("callback null"); |
| } |
| mAudioParamListeners.addBinder(callback); |
| } |
| |
| @Override |
| public void unregisterOnParameterChangeListener(ICarAudioCallback callback) { |
| if (callback == null) { |
| return; |
| } |
| mAudioParamListeners.removeBinder(callback); |
| } |
| |
| private void populateParameterKeysLocked() { |
| String[] keys = mAudioHal.getAudioParameterKeys(); |
| mAudioParamKeys = new HashSet<>(); |
| if (keys == null) { // not supported |
| return; |
| } |
| for (String key : keys) { |
| mAudioParamKeys.add(key); |
| } |
| } |
| |
| private void assertPamameterKeysLocked(String key) { |
| if (!mAudioParamKeys.contains(key)) { |
| throw new IllegalArgumentException("Audio parameter not available:" + key); |
| } |
| } |
| |
| private void enforceAudioSettingsPermission() { |
| if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException( |
| "requires permission " + Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS); |
| } |
| } |
| |
| /** |
| * API for system to control mute with lock. |
| * @param mute |
| * @return the current mute state |
| */ |
| public void muteMediaWithLock(boolean lock) { |
| mMediaMuteAudioFocusListener.mute(lock); |
| } |
| |
| public void unMuteMedia() { |
| // unmute always done with lock |
| mMediaMuteAudioFocusListener.unMute(true); |
| } |
| |
| public AudioRoutingPolicy getAudioRoutingPolicy() { |
| return mAudioRoutingPolicy; |
| } |
| |
| private void enforceAudioVolumePermission() { |
| if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException( |
| "requires permission " + Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME); |
| } |
| } |
| |
| private void doHandleCarFocusChange() { |
| int newFocusState = AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_INVALID; |
| FocusState currentState; |
| AudioFocusInfo topInfo; |
| boolean systemSoundActive = false; |
| synchronized (mLock) { |
| if (mFocusReceived == null) { |
| // already handled |
| return; |
| } |
| if (mFocusReceived.equals(mCurrentFocusState)) { |
| // no change |
| mFocusReceived = null; |
| return; |
| } |
| if (DBG) { |
| Log.d(TAG_FOCUS, "focus change from car:" + mFocusReceived); |
| } |
| systemSoundActive = mSystemSoundPhysicalStreamActive; |
| topInfo = mTopFocusInfo; |
| if (!mFocusReceived.equals(mCurrentFocusState.focusState)) { |
| newFocusState = mFocusReceived.focusState; |
| } |
| mCurrentFocusState = mFocusReceived; |
| currentState = mFocusReceived; |
| mFocusReceived = null; |
| if (mLastFocusRequestToCar != null && |
| (mLastFocusRequestToCar.focusRequest == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN || |
| mLastFocusRequestToCar.focusRequest == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT || |
| mLastFocusRequestToCar.focusRequest == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_MAY_DUCK) && |
| (mCurrentFocusState.streams & mLastFocusRequestToCar.streams) != |
| mLastFocusRequestToCar.streams) { |
| Log.w(TAG_FOCUS, "streams mismatch, requested:0x" + Integer.toHexString( |
| mLastFocusRequestToCar.streams) + " got:0x" + |
| Integer.toHexString(mCurrentFocusState.streams)); |
| // treat it as focus loss as requested streams are not there. |
| newFocusState = AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS; |
| } |
| mLastFocusRequestToCar = null; |
| if (mRadioOrExtSourceActive && |
| (mCurrentFocusState.externalFocus & |
| AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG) == 0) { |
| // radio flag dropped |
| newFocusState = AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS; |
| mRadioOrExtSourceActive = false; |
| } |
| if (newFocusState == AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS || |
| newFocusState == AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT || |
| newFocusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_EXLCUSIVE) { |
| // clear second one as there can be no such item in these LOSS. |
| mSecondFocusInfo = null; |
| } |
| } |
| switch (newFocusState) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN: |
| doHandleFocusGainFromCar(currentState, topInfo, systemSoundActive); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN_TRANSIENT: |
| doHandleFocusGainTransientFromCar(currentState, topInfo, systemSoundActive); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS: |
| doHandleFocusLossFromCar(currentState, topInfo); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT: |
| doHandleFocusLossTransientFromCar(currentState); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_CAN_DUCK: |
| doHandleFocusLossTransientCanDuckFromCar(currentState); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_EXLCUSIVE: |
| doHandleFocusLossTransientExclusiveFromCar(currentState); |
| break; |
| } |
| } |
| |
| private void doHandleFocusGainFromCar(FocusState currentState, AudioFocusInfo topInfo, |
| boolean systemSoundActive) { |
| if (isFocusFromCarServiceBottom(topInfo)) { |
| if (systemSoundActive) { // focus requested for system sound |
| if (DBG) { |
| Log.d(TAG_FOCUS, "focus gain due to system sound"); |
| } |
| return; |
| } |
| Log.w(TAG_FOCUS, "focus gain from car:" + currentState + |
| " while bottom listener is top"); |
| mFocusHandler.handleFocusReleaseRequest(); |
| } else { |
| mAudioManager.abandonAudioFocus(mCarProxyAudioFocusListener); |
| } |
| } |
| |
| private void doHandleFocusGainTransientFromCar(FocusState currentState, |
| AudioFocusInfo topInfo, boolean systemSoundActive) { |
| if ((currentState.externalFocus & |
| (AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_PERMANENT_FLAG | |
| AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_TRANSIENT_FLAG)) == 0) { |
| mAudioManager.abandonAudioFocus(mCarProxyAudioFocusListener); |
| } else { |
| if (isFocusFromCarServiceBottom(topInfo) || isFocusFromCarProxy(topInfo)) { |
| if (systemSoundActive) { // focus requested for system sound |
| if (DBG) { |
| Log.d(TAG_FOCUS, "focus gain tr due to system sound"); |
| } |
| return; |
| } |
| Log.w(TAG_FOCUS, "focus gain transient from car:" + currentState + |
| " while bottom listener or car proxy is top"); |
| mFocusHandler.handleFocusReleaseRequest(); |
| } |
| } |
| } |
| |
| private void doHandleFocusLossFromCar(FocusState currentState, AudioFocusInfo topInfo) { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "doHandleFocusLossFromCar current:" + currentState + |
| " top:" + dumpAudioFocusInfo(topInfo)); |
| } |
| boolean shouldRequestProxyFocus = false; |
| if ((currentState.externalFocus & |
| AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_PERMANENT_FLAG) != 0) { |
| shouldRequestProxyFocus = true; |
| } |
| if (isFocusFromCarProxy(topInfo)) { |
| // already car proxy is top. Nothing to do. |
| return; |
| } else if (!isFocusFromCarServiceBottom(topInfo)) { |
| shouldRequestProxyFocus = true; |
| } |
| if (shouldRequestProxyFocus) { |
| requestCarProxyFocus(AudioManager.AUDIOFOCUS_GAIN, 0); |
| } |
| } |
| |
| private void doHandleFocusLossTransientFromCar(FocusState currentState) { |
| requestCarProxyFocus(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, 0); |
| } |
| |
| private void doHandleFocusLossTransientCanDuckFromCar(FocusState currentState) { |
| requestCarProxyFocus(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, 0); |
| } |
| |
| private void doHandleFocusLossTransientExclusiveFromCar(FocusState currentState) { |
| requestCarProxyFocus(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, |
| AudioManager.AUDIOFOCUS_FLAG_LOCK); |
| } |
| |
| private void requestCarProxyFocus(int androidFocus, int flags) { |
| mAudioManager.requestAudioFocus(mCarProxyAudioFocusListener, mAttributeCarExternal, |
| androidFocus, flags, mAudioPolicy); |
| } |
| |
| private void doHandleStreamStatusChange(int streamNumber, boolean streamActive) { |
| synchronized (mLock) { |
| if (streamNumber != mSystemSoundPhysicalStream) { |
| return; |
| } |
| mSystemSoundPhysicalStreamActive = streamActive; |
| } |
| doHandleAndroidFocusChange(true /*triggeredByStreamChange*/); |
| } |
| |
| private boolean isFocusFromCarServiceBottom(AudioFocusInfo info) { |
| if (info == null) { |
| return false; |
| } |
| AudioAttributes attrib = info.getAttributes(); |
| if (info.getPackageName().equals(mContext.getOpPackageName()) && |
| CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib) == |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_BOTTOM) { |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isFocusFromCarProxy(AudioFocusInfo info) { |
| if (info == null) { |
| return false; |
| } |
| AudioAttributes attrib = info.getAttributes(); |
| if (info.getPackageName().equals(mContext.getOpPackageName()) && |
| attrib != null && |
| CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib) == |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_CAR_PROXY) { |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isFocusFromExternalRadioOrExternalSource(AudioFocusInfo info) { |
| if (info == null) { |
| return false; |
| } |
| AudioAttributes attrib = info.getAttributes(); |
| if (attrib == null) { |
| return false; |
| } |
| // if radio is not external, no special handling of radio is necessary. |
| if (CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib) == |
| CarAudioManager.CAR_AUDIO_USAGE_RADIO && mIsRadioExternal) { |
| return true; |
| } else if (CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib) == |
| CarAudioManager.CAR_AUDIO_USAGE_EXTERNAL_AUDIO_SOURCE) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Re-evaluate current focus state and send focus request to car if new focus was requested. |
| * @return true if focus change was requested to car. |
| */ |
| private boolean reevaluateCarAudioFocusAndSendFocusLocked() { |
| if (mTopFocusInfo == null) { |
| if (mSystemSoundPhysicalStreamActive) { |
| return requestFocusForSystemSoundOnlyCaseLocked(); |
| } else { |
| requestFocusReleaseForSystemSoundLocked(); |
| return false; |
| } |
| } |
| if (mTopFocusInfo.getLossReceived() != 0) { |
| // top one got loss. This should not happen. |
| Log.e(TAG_FOCUS, "Top focus holder got loss " + dumpAudioFocusInfo(mTopFocusInfo)); |
| return false; |
| } |
| if (isFocusFromCarServiceBottom(mTopFocusInfo) || isFocusFromCarProxy(mTopFocusInfo)) { |
| // allow system sound only when car is not holding focus. |
| if (mSystemSoundPhysicalStreamActive && isFocusFromCarServiceBottom(mTopFocusInfo)) { |
| return requestFocusForSystemSoundOnlyCaseLocked(); |
| } |
| switch (mCurrentFocusState.focusState) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN_TRANSIENT: |
| //should not have focus. So enqueue release |
| mFocusHandler.handleFocusReleaseRequest(); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS: |
| doHandleFocusLossFromCar(mCurrentFocusState, mTopFocusInfo); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT: |
| doHandleFocusLossTransientFromCar(mCurrentFocusState); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_CAN_DUCK: |
| doHandleFocusLossTransientCanDuckFromCar(mCurrentFocusState); |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_EXLCUSIVE: |
| doHandleFocusLossTransientExclusiveFromCar(mCurrentFocusState); |
| break; |
| } |
| mRadioOrExtSourceActive = false; |
| return false; |
| } |
| mFocusHandler.cancelFocusReleaseRequest(); |
| AudioAttributes attrib = mTopFocusInfo.getAttributes(); |
| int logicalStreamTypeForTop = CarAudioAttributesUtil.getCarUsageFromAudioAttributes(attrib); |
| int physicalStreamTypeForTop = mAudioRoutingPolicy.getPhysicalStreamForLogicalStream( |
| (logicalStreamTypeForTop < CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_BOTTOM) |
| ? logicalStreamTypeForTop : CarAudioManager.CAR_AUDIO_USAGE_MUSIC); |
| |
| boolean muteMedia = false; |
| String primaryExtSource = CarAudioAttributesUtil.getExtRouting(attrib); |
| // update primary context and notify if necessary |
| int primaryContext = AudioHalService.logicalStreamWithExtTypeToHalContextType( |
| logicalStreamTypeForTop, primaryExtSource); |
| if (logicalStreamTypeForTop == |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_MEDIA_MUTE) { |
| muteMedia = true; |
| } |
| if (logicalStreamTypeForTop == CarAudioManager.CAR_AUDIO_USAGE_VOICE_CALL) { |
| mCallActive = true; |
| } else { |
| mCallActive = false; |
| } |
| // other apps having focus |
| int focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE; |
| int extFocus = AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_NONE_FLAG; |
| int streamsToRequest = 0x1 << physicalStreamTypeForTop; |
| boolean primaryIsExternal = false; |
| if (isFocusFromExternalRadioOrExternalSource(mTopFocusInfo)) { |
| streamsToRequest = 0; |
| mRadioOrExtSourceActive = true; |
| primaryIsExternal = true; |
| if (fixExtSourceAndContext( |
| mExtSourceInfoScratch.set(primaryExtSource, primaryContext))) { |
| primaryExtSource = mExtSourceInfoScratch.source; |
| primaryContext = mExtSourceInfoScratch.context; |
| } |
| } else { |
| mRadioOrExtSourceActive = false; |
| primaryExtSource = null; |
| } |
| // save the current context now but it is sent to context change listener after focus |
| // response from car |
| if (mCurrentPrimaryAudioContext != primaryContext) { |
| mCurrentPrimaryAudioContext = primaryContext; |
| mCurrentPrimaryPhysicalStream = physicalStreamTypeForTop; |
| } |
| |
| boolean secondaryIsExternal = false; |
| int secondaryContext = 0; |
| String secondaryExtSource = null; |
| switch (mTopFocusInfo.getGainRequest()) { |
| case AudioManager.AUDIOFOCUS_GAIN: |
| focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN; |
| break; |
| case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: |
| case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: |
| focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT; |
| break; |
| case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: |
| focusToRequest = |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_MAY_DUCK; |
| if (mSecondFocusInfo == null) { |
| break; |
| } |
| AudioAttributes secondAttrib = mSecondFocusInfo.getAttributes(); |
| if (secondAttrib == null) { |
| break; |
| } |
| int logicalStreamTypeForSecond = |
| CarAudioAttributesUtil.getCarUsageFromAudioAttributes(secondAttrib); |
| if (logicalStreamTypeForSecond == |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_MEDIA_MUTE) { |
| muteMedia = true; |
| break; |
| } |
| if (isFocusFromExternalRadioOrExternalSource(mSecondFocusInfo)) { |
| secondaryIsExternal = true; |
| secondaryExtSource = CarAudioAttributesUtil.getExtRouting(secondAttrib); |
| secondaryContext = AudioHalService.logicalStreamWithExtTypeToHalContextType( |
| logicalStreamTypeForSecond, secondaryExtSource); |
| if (fixExtSourceAndContext( |
| mExtSourceInfoScratch.set(secondaryExtSource, secondaryContext))) { |
| secondaryExtSource = mExtSourceInfoScratch.source; |
| secondaryContext = mExtSourceInfoScratch.context; |
| } |
| int secondaryExtPhysicalStreamFlag = |
| getPhysicalStreamFlagForExtSourceLocked(secondaryExtSource); |
| if ((secondaryExtPhysicalStreamFlag & streamsToRequest) != 0) { |
| // secondary stream is the same as primary. cannot keep secondary |
| secondaryIsExternal = false; |
| secondaryContext = 0; |
| secondaryExtSource = null; |
| break; |
| } |
| mRadioOrExtSourceActive = true; |
| } else { |
| secondaryContext = AudioHalService.logicalStreamWithExtTypeToHalContextType( |
| logicalStreamTypeForSecond, null); |
| } |
| switch (mCurrentFocusState.focusState) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN: |
| streamsToRequest |= mCurrentFocusState.streams; |
| focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN; |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN_TRANSIENT: |
| streamsToRequest |= mCurrentFocusState.streams; |
| focusToRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT; |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_CAN_DUCK: |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_EXLCUSIVE: |
| doHandleFocusLossTransientExclusiveFromCar(mCurrentFocusState); |
| return false; |
| } |
| break; |
| default: |
| streamsToRequest = 0; |
| break; |
| } |
| int audioContexts = 0; |
| if (muteMedia) { |
| boolean addMute = true; |
| if (primaryIsExternal) { |
| if ((getPhysicalStreamFlagForExtSourceLocked(primaryExtSource) & |
| (0x1 << mRadioPhysicalStream)) != 0) { |
| // cannot mute as primary is media |
| addMute = false; |
| } |
| } else if (secondaryIsExternal) { |
| if ((getPhysicalStreamFlagForExtSourceLocked(secondaryExtSource) & |
| (0x1 << mRadioPhysicalStream)) != 0) { |
| mRadioOrExtSourceActive = false; |
| } |
| } else { |
| mRadioOrExtSourceActive = false; |
| } |
| audioContexts = primaryContext | secondaryContext; |
| if (addMute) { |
| audioContexts &= ~(AudioHalService.AUDIO_CONTEXT_RADIO_FLAG | |
| AudioHalService.AUDIO_CONTEXT_MUSIC_FLAG | |
| AudioHalService.AUDIO_CONTEXT_CD_ROM_FLAG | |
| AudioHalService.AUDIO_CONTEXT_AUX_AUDIO_FLAG); |
| extFocus = AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_MUTE_MEDIA_FLAG; |
| streamsToRequest &= ~(0x1 << mRadioPhysicalStream); |
| } |
| } else if (mRadioOrExtSourceActive) { |
| boolean addExtFocusFlag = true; |
| if (primaryIsExternal) { |
| int primaryExtPhysicalStreamFlag = |
| getPhysicalStreamFlagForExtSourceLocked(primaryExtSource); |
| if (secondaryIsExternal) { |
| int secondaryPhysicalStreamFlag = |
| getPhysicalStreamFlagForExtSourceLocked(secondaryExtSource); |
| if (primaryExtPhysicalStreamFlag == secondaryPhysicalStreamFlag) { |
| // overlap, drop secondary |
| audioContexts &= ~secondaryContext; |
| secondaryContext = 0; |
| secondaryExtSource = null; |
| } |
| streamsToRequest = 0; |
| } else { // primary only |
| if (streamsToRequest == primaryExtPhysicalStreamFlag) { |
| // cannot keep secondary |
| secondaryContext = 0; |
| } |
| streamsToRequest &= ~primaryExtPhysicalStreamFlag; |
| } |
| } |
| if (addExtFocusFlag) { |
| extFocus = AudioHalService.VEHICLE_AUDIO_EXT_FOCUS_CAR_PLAY_ONLY_FLAG; |
| } |
| audioContexts = primaryContext | secondaryContext; |
| } else if (streamsToRequest == 0) { |
| if (mSystemSoundPhysicalStreamActive) { |
| return requestFocusForSystemSoundOnlyCaseLocked(); |
| } else { |
| mCurrentAudioContexts = 0; |
| mFocusHandler.handleFocusReleaseRequest(); |
| return false; |
| } |
| } else { |
| audioContexts = primaryContext | secondaryContext; |
| } |
| if (mSystemSoundPhysicalStreamActive) { |
| boolean addSystemStream = true; |
| if (primaryIsExternal && getPhysicalStreamNumberForExtSourceLocked(primaryExtSource) == |
| mSystemSoundPhysicalStream) { |
| addSystemStream = false; |
| } |
| if (secondaryIsExternal && getPhysicalStreamNumberForExtSourceLocked(secondaryExtSource) |
| == mSystemSoundPhysicalStream) { |
| addSystemStream = false; |
| } |
| int systemSoundFlag = 0x1 << mSystemSoundPhysicalStream; |
| // stream already added by focus. Cannot distinguish system sound play from other sound |
| // in this stream. |
| if ((streamsToRequest & systemSoundFlag) != 0) { |
| addSystemStream = false; |
| } |
| if (addSystemStream) { |
| streamsToRequest |= systemSoundFlag; |
| audioContexts |= AudioHalService.AUDIO_CONTEXT_SYSTEM_SOUND_FLAG; |
| if (focusToRequest == AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE) { |
| focusToRequest = |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_NO_DUCK; |
| } |
| } |
| } |
| boolean routingHintChanged = sendExtRoutingHintToCarIfNecessaryLocked(primaryExtSource, |
| secondaryExtSource); |
| return sendFocusRequestToCarIfNecessaryLocked(focusToRequest, streamsToRequest, extFocus, |
| audioContexts, routingHintChanged); |
| } |
| |
| /** |
| * Fix external source info if it is not valid. |
| * @param extSourceInfo |
| * @return true if value is not valid and was updated. |
| */ |
| private boolean fixExtSourceAndContext(ExtSourceInfo extSourceInfo) { |
| if (!mExternalRoutingTypes.containsKey(extSourceInfo.source)) { |
| Log.w(CarLog.TAG_AUDIO, "External source not available:" + extSourceInfo.source); |
| // fall back to radio |
| extSourceInfo.source = mDefaultRadioRoutingType; |
| extSourceInfo.context = AudioHalService.AUDIO_CONTEXT_RADIO_FLAG; |
| return true; |
| } |
| if (extSourceInfo.context == AudioHalService.AUDIO_CONTEXT_RADIO_FLAG && |
| !extSourceInfo.source.startsWith("RADIO_")) { |
| Log.w(CarLog.TAG_AUDIO, "Expecting Radio source:" + extSourceInfo.source); |
| extSourceInfo.source = mDefaultRadioRoutingType; |
| return true; |
| } |
| return false; |
| } |
| |
| private int getPhysicalStreamFlagForExtSourceLocked(String extSource) { |
| AudioHalService.ExtRoutingSourceInfo info = mExternalRoutingTypes.get( |
| extSource); |
| if (info != null) { |
| return 0x1 << info.physicalStreamNumber; |
| } else { |
| return 0x1 << mRadioPhysicalStream; |
| } |
| } |
| |
| private int getPhysicalStreamNumberForExtSourceLocked(String extSource) { |
| AudioHalService.ExtRoutingSourceInfo info = mExternalRoutingTypes.get( |
| extSource); |
| if (info != null) { |
| return info.physicalStreamNumber; |
| } else { |
| return mRadioPhysicalStream; |
| } |
| } |
| |
| private boolean sendExtRoutingHintToCarIfNecessaryLocked(String primarySource, |
| String secondarySource) { |
| if (!mExternalRoutingHintSupported) { |
| return false; |
| } |
| if (DBG) { |
| Log.d(TAG_FOCUS, "Setting external routing hint, primary:" + primarySource + |
| " secondary:" + secondarySource); |
| } |
| Arrays.fill(mExternalRoutingsScratch, 0); |
| fillExtRoutingPositionLocked(mExternalRoutingsScratch, primarySource); |
| fillExtRoutingPositionLocked(mExternalRoutingsScratch, secondarySource); |
| if (Arrays.equals(mExternalRoutingsScratch, mExternalRoutings)) { |
| return false; |
| } |
| System.arraycopy(mExternalRoutingsScratch, 0, mExternalRoutings, 0, |
| mExternalRoutingsScratch.length); |
| if (DBG) { |
| Log.d(TAG_FOCUS, "Set values:" + Arrays.toString(mExternalRoutingsScratch)); |
| } |
| try { |
| mAudioHal.setExternalRoutingSource(mExternalRoutings); |
| } catch (IllegalArgumentException e) { |
| //ignore. can happen with mocking. |
| return false; |
| } |
| return true; |
| } |
| |
| private void fillExtRoutingPositionLocked(int[] array, String extSource) { |
| if (extSource == null) { |
| return; |
| } |
| AudioHalService.ExtRoutingSourceInfo info = mExternalRoutingTypes.get( |
| extSource); |
| if (info == null) { |
| return; |
| } |
| int pos = info.bitPosition; |
| if (pos < 0) { |
| return; |
| } |
| int index = pos / 32; |
| int bitPosInInt = pos % 32; |
| array[index] |= (0x1 << bitPosInInt); |
| } |
| |
| private boolean requestFocusForSystemSoundOnlyCaseLocked() { |
| int focusRequest = AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_NO_DUCK; |
| int streamsToRequest = 0x1 << mSystemSoundPhysicalStream; |
| int extFocus = 0; |
| int audioContexts = AudioHalService.AUDIO_CONTEXT_SYSTEM_SOUND_FLAG; |
| mCurrentPrimaryAudioContext = audioContexts; |
| return sendFocusRequestToCarIfNecessaryLocked(focusRequest, streamsToRequest, extFocus, |
| audioContexts, false /*forceSend*/); |
| } |
| |
| private void requestFocusReleaseForSystemSoundLocked() { |
| switch (mCurrentFocusState.focusState) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN_TRANSIENT: |
| mFocusHandler.handleFocusReleaseRequest(); |
| default: // ignore |
| break; |
| } |
| } |
| |
| private boolean sendFocusRequestToCarIfNecessaryLocked(int focusToRequest, |
| int streamsToRequest, int extFocus, int audioContexts, boolean forceSend) { |
| if (needsToSendFocusRequestLocked(focusToRequest, streamsToRequest, extFocus, |
| audioContexts) || forceSend) { |
| mLastFocusRequestToCar = FocusRequest.create(focusToRequest, streamsToRequest, |
| extFocus); |
| mCurrentAudioContexts = audioContexts; |
| if (((mCurrentFocusState.streams & streamsToRequest) == streamsToRequest) && |
| ((mCurrentFocusState.streams & ~streamsToRequest) != 0)) { |
| // stream is reduced, so do not release it immediately |
| try { |
| Thread.sleep(NO_FOCUS_PLAY_WAIT_TIME_MS); |
| } catch (InterruptedException e) { |
| // ignore |
| } |
| } |
| if (DBG) { |
| Log.d(TAG_FOCUS, "focus request to car:" + mLastFocusRequestToCar + " context:0x" + |
| Integer.toHexString(audioContexts)); |
| } |
| try { |
| mAudioHal.requestAudioFocusChange(focusToRequest, streamsToRequest, extFocus, |
| audioContexts); |
| } catch (IllegalArgumentException e) { |
| // can happen when mocking ends. ignore. timeout will handle it properly. |
| } |
| try { |
| mLock.wait(mFocusResponseWaitTimeoutMs); |
| } catch (InterruptedException e) { |
| //ignore |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean needsToSendFocusRequestLocked(int focusToRequest, int streamsToRequest, |
| int extFocus, int audioContexts) { |
| if (streamsToRequest != mCurrentFocusState.streams) { |
| return true; |
| } |
| if (audioContexts != mCurrentAudioContexts) { |
| return true; |
| } |
| if ((extFocus & mCurrentFocusState.externalFocus) != extFocus) { |
| return true; |
| } |
| switch (focusToRequest) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN: |
| if (mCurrentFocusState.focusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN) { |
| return false; |
| } |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_MAY_DUCK: |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_GAIN_TRANSIENT_NO_DUCK: |
| if (mCurrentFocusState.focusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN || |
| mCurrentFocusState.focusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_GAIN_TRANSIENT) { |
| return false; |
| } |
| break; |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE: |
| if (mCurrentFocusState.focusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS || |
| mCurrentFocusState.focusState == |
| AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS_TRANSIENT_EXLCUSIVE) { |
| return false; |
| } |
| break; |
| } |
| return true; |
| } |
| |
| private void doHandleAndroidFocusChange(boolean triggeredByStreamChange) { |
| boolean focusRequested = false; |
| synchronized (mLock) { |
| AudioFocusInfo newTopInfo = null; |
| if (mPendingFocusChanges.isEmpty()) { |
| if (!triggeredByStreamChange) { |
| // no entry. It was handled already. |
| if (DBG) { |
| Log.d(TAG_FOCUS, "doHandleAndroidFocusChange, mPendingFocusChanges empty"); |
| } |
| return; |
| } |
| } else { |
| newTopInfo = mPendingFocusChanges.getFirst(); |
| mPendingFocusChanges.clear(); |
| if (mTopFocusInfo != null && |
| newTopInfo.getClientId().equals(mTopFocusInfo.getClientId()) && |
| newTopInfo.getGainRequest() == mTopFocusInfo.getGainRequest() && |
| isAudioAttributesSame( |
| newTopInfo.getAttributes(), mTopFocusInfo.getAttributes()) && |
| !triggeredByStreamChange) { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "doHandleAndroidFocusChange, no change in top state:" + |
| dumpAudioFocusInfo(mTopFocusInfo)); |
| } |
| // already in top somehow, no need to make any change |
| return; |
| } |
| } |
| if (newTopInfo != null) { |
| if (newTopInfo.getGainRequest() == |
| AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) { |
| mSecondFocusInfo = mTopFocusInfo; |
| } else { |
| mSecondFocusInfo = null; |
| } |
| if (DBG) { |
| Log.d(TAG_FOCUS, "top focus changed to:" + dumpAudioFocusInfo(newTopInfo)); |
| } |
| mTopFocusInfo = newTopInfo; |
| } |
| focusRequested = handleCarFocusRequestAndResponseLocked(); |
| } |
| // handle it if there was response or force handle it for timeout. |
| if (focusRequested) { |
| doHandleCarFocusChange(); |
| } |
| } |
| |
| private boolean handleCarFocusRequestAndResponseLocked() { |
| boolean focusRequested = reevaluateCarAudioFocusAndSendFocusLocked(); |
| if (DBG) { |
| if (!focusRequested) { |
| Log.i(TAG_FOCUS, "focus not requested for top focus:" + |
| dumpAudioFocusInfo(mTopFocusInfo) + " currentState:" + mCurrentFocusState); |
| } |
| } |
| if (focusRequested) { |
| if (mFocusReceived == null) { |
| Log.w(TAG_FOCUS, "focus response timed out, request sent " |
| + mLastFocusRequestToCar); |
| // no response. so reset to loss. |
| mFocusReceived = FocusState.STATE_LOSS; |
| mCurrentAudioContexts = 0; |
| mNumConsecutiveHalFailures++; |
| mCurrentPrimaryAudioContext = 0; |
| mCurrentPrimaryPhysicalStream = 0; |
| } else { |
| mNumConsecutiveHalFailures = 0; |
| } |
| // send context change after getting focus response. |
| if (mCarAudioContextChangeHandler != null) { |
| mCarAudioContextChangeHandler.requestContextChangeNotification( |
| mAudioContextChangeListener, mCurrentPrimaryAudioContext, |
| mCurrentPrimaryPhysicalStream); |
| } |
| checkCanStatus(); |
| } |
| return focusRequested; |
| } |
| |
| private void doHandleFocusRelease() { |
| boolean sent = false; |
| synchronized (mLock) { |
| if (mCurrentFocusState != FocusState.STATE_LOSS) { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "focus release to car"); |
| } |
| mLastFocusRequestToCar = FocusRequest.STATE_RELEASE; |
| sent = true; |
| try { |
| if (mExternalRoutingHintSupported) { |
| mAudioHal.setExternalRoutingSource(mExternalRoutingsForFocusRelease); |
| } |
| mAudioHal.requestAudioFocusChange( |
| AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE, 0, 0); |
| } catch (IllegalArgumentException e) { |
| // can happen when mocking ends. ignore. timeout will handle it properly. |
| } |
| try { |
| mLock.wait(mFocusResponseWaitTimeoutMs); |
| } catch (InterruptedException e) { |
| //ignore |
| } |
| mCurrentPrimaryAudioContext = 0; |
| mCurrentPrimaryPhysicalStream = 0; |
| if (mCarAudioContextChangeHandler != null) { |
| mCarAudioContextChangeHandler.requestContextChangeNotification( |
| mAudioContextChangeListener, mCurrentPrimaryAudioContext, |
| mCurrentPrimaryPhysicalStream); |
| } |
| } else if (DBG) { |
| Log.d(TAG_FOCUS, "doHandleFocusRelease: do not send, already loss"); |
| } |
| } |
| // handle it if there was response. |
| if (sent) { |
| doHandleCarFocusChange(); |
| } |
| } |
| |
| private void checkCanStatus() { |
| if (mCanBusErrorNotifier == null) { |
| // TODO(b/36189057): create CanBusErrorNotifier from unit-tests and remove this code |
| return; |
| } |
| |
| // If CAN bus recovers, message will be removed. |
| if (mNumConsecutiveHalFailures >= mNumConsecutiveHalFailuresForCanError) { |
| mCanBusErrorNotifier.reportFailure(this); |
| } else { |
| mCanBusErrorNotifier.removeFailureReport(this); |
| } |
| } |
| |
| private static boolean isAudioAttributesSame(AudioAttributes one, AudioAttributes two) { |
| if (one.getContentType() != two.getContentType()) { |
| return false; |
| } |
| if (one.getUsage() != two.getUsage()) { |
| return false; |
| } |
| return true; |
| } |
| |
| private static String dumpAudioFocusInfo(AudioFocusInfo info) { |
| if (info == null) { |
| return "null"; |
| } |
| StringBuilder builder = new StringBuilder(); |
| builder.append("afi package:" + info.getPackageName()); |
| builder.append("client id:" + info.getClientId()); |
| builder.append(",gain:" + info.getGainRequest()); |
| builder.append(",loss:" + info.getLossReceived()); |
| builder.append(",flag:" + info.getFlags()); |
| AudioAttributes attrib = info.getAttributes(); |
| if (attrib != null) { |
| builder.append("," + attrib.toString()); |
| } |
| return builder.toString(); |
| } |
| |
| private class SystemFocusListener extends AudioPolicyFocusListener { |
| @Override |
| public void onAudioFocusGrant(AudioFocusInfo afi, int requestResult) { |
| if (afi == null) { |
| return; |
| } |
| if (DBG) { |
| Log.d(TAG_FOCUS, "onAudioFocusGrant " + dumpAudioFocusInfo(afi) + |
| " result:" + requestResult); |
| } |
| if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| synchronized (mLock) { |
| mPendingFocusChanges.addFirst(afi); |
| } |
| mFocusHandler.handleAndroidFocusChange(); |
| } |
| } |
| |
| @Override |
| public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "onAudioFocusLoss " + dumpAudioFocusInfo(afi) + |
| " notified:" + wasNotified); |
| } |
| // ignore loss as tracking gain is enough. At least bottom listener will be |
| // always there and getting focus grant. So it is safe to ignore this here. |
| } |
| } |
| |
| /** |
| * Focus listener to take focus away from android apps as a proxy to car. |
| */ |
| private class CarProxyAndroidFocusListener implements AudioManager.OnAudioFocusChangeListener { |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| // Do not need to handle car's focus loss or gain separately. Focus monitoring |
| // through system focus listener will take care all cases. |
| } |
| } |
| |
| /** |
| * Focus listener kept at the bottom to check if there is any focus holder. |
| * |
| */ |
| private class BottomAudioFocusListener implements AudioManager.OnAudioFocusChangeListener { |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| synchronized (mLock) { |
| mBottomFocusState = focusChange; |
| } |
| } |
| } |
| |
| private class MediaMuteAudioFocusListener implements AudioManager.OnAudioFocusChangeListener { |
| |
| private final AudioAttributes mMuteAudioAttrib = |
| CarAudioAttributesUtil.getAudioAttributesForCarUsage( |
| CarAudioAttributesUtil.CAR_AUDIO_USAGE_CARSERVICE_MEDIA_MUTE); |
| |
| /** not muted */ |
| private final static int MUTE_STATE_UNMUTED = 0; |
| /** muted. other app requesting focus GAIN will unmute it */ |
| private final static int MUTE_STATE_MUTED = 1; |
| /** locked. only system can unlock and send it to muted or unmuted state */ |
| private final static int MUTE_STATE_LOCKED = 2; |
| |
| private int mMuteState = MUTE_STATE_UNMUTED; |
| |
| @Override |
| public void onAudioFocusChange(int focusChange) { |
| if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { |
| // mute does not persist when there is other media kind app taking focus |
| unMute(); |
| } |
| } |
| |
| public boolean mute() { |
| return mute(false); |
| } |
| |
| /** |
| * Mute with optional lock |
| * @param lock Take focus with lock. Normal apps cannot take focus. Setting this will |
| * essentially mute all audio. |
| * @return Final mute state |
| */ |
| public synchronized boolean mute(boolean lock) { |
| int result = AudioManager.AUDIOFOCUS_REQUEST_FAILED; |
| boolean lockRequested = false; |
| if (lock) { |
| AudioPolicy audioPolicy = null; |
| synchronized (CarAudioService.this) { |
| audioPolicy = mAudioPolicy; |
| } |
| if (audioPolicy != null) { |
| result = mAudioManager.requestAudioFocus(this, mMuteAudioAttrib, |
| AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, |
| AudioManager.AUDIOFOCUS_FLAG_LOCK | |
| AudioManager.AUDIOFOCUS_FLAG_DELAY_OK, |
| audioPolicy); |
| lockRequested = true; |
| } |
| } |
| if (!lockRequested) { |
| result = mAudioManager.requestAudioFocus(this, mMuteAudioAttrib, |
| AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, |
| AudioManager.AUDIOFOCUS_FLAG_DELAY_OK); |
| } |
| if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED || |
| result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) { |
| if (lockRequested) { |
| mMuteState = MUTE_STATE_LOCKED; |
| } else { |
| mMuteState = MUTE_STATE_MUTED; |
| } |
| } else { |
| mMuteState = MUTE_STATE_UNMUTED; |
| } |
| return mMuteState != MUTE_STATE_UNMUTED; |
| } |
| |
| public boolean unMute() { |
| return unMute(false); |
| } |
| |
| /** |
| * Unmute. If locked, unmute will only succeed when unlock is set to true. |
| * @param unlock |
| * @return Final mute state |
| */ |
| public synchronized boolean unMute(boolean unlock) { |
| if (!unlock && mMuteState == MUTE_STATE_LOCKED) { |
| // cannot unlock |
| return true; |
| } |
| mMuteState = MUTE_STATE_UNMUTED; |
| mAudioManager.abandonAudioFocus(this); |
| return false; |
| } |
| |
| public synchronized boolean isMuted() { |
| return mMuteState != MUTE_STATE_UNMUTED; |
| } |
| } |
| |
| private class CarAudioContextChangeHandler extends Handler { |
| private static final int MSG_CONTEXT_CHANGE = 0; |
| |
| private CarAudioContextChangeHandler(Looper looper) { |
| super(looper); |
| } |
| |
| private void requestContextChangeNotification(AudioContextChangeListener listener, |
| int primaryContext, int physicalStream) { |
| Message msg = obtainMessage(MSG_CONTEXT_CHANGE, primaryContext, physicalStream, |
| listener); |
| sendMessage(msg); |
| } |
| |
| private void cancelAll() { |
| removeMessages(MSG_CONTEXT_CHANGE); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_CONTEXT_CHANGE: { |
| AudioContextChangeListener listener = (AudioContextChangeListener) msg.obj; |
| int context = msg.arg1; |
| int physicalStream = msg.arg2; |
| listener.onContextChange(context, physicalStream); |
| } break; |
| } |
| } |
| } |
| |
| private class CarAudioFocusChangeHandler extends Handler { |
| private static final int MSG_FOCUS_CHANGE = 0; |
| private static final int MSG_STREAM_STATE_CHANGE = 1; |
| private static final int MSG_ANDROID_FOCUS_CHANGE = 2; |
| private static final int MSG_FOCUS_RELEASE = 3; |
| |
| /** Focus release is always delayed this much to handle repeated acquire / release. */ |
| private static final long FOCUS_RELEASE_DELAY_MS = 500; |
| |
| private CarAudioFocusChangeHandler(Looper looper) { |
| super(looper); |
| } |
| |
| private void handleFocusChange() { |
| cancelFocusReleaseRequest(); |
| Message msg = obtainMessage(MSG_FOCUS_CHANGE); |
| sendMessage(msg); |
| } |
| |
| private void handleStreamStateChange(int streamNumber, boolean streamActive) { |
| cancelFocusReleaseRequest(); |
| removeMessages(MSG_STREAM_STATE_CHANGE); |
| Message msg = obtainMessage(MSG_STREAM_STATE_CHANGE, streamNumber, |
| streamActive ? 1 : 0); |
| sendMessageDelayed(msg, |
| streamActive ? NO_FOCUS_PLAY_WAIT_TIME_MS : FOCUS_RELEASE_DELAY_MS); |
| } |
| |
| private void handleAndroidFocusChange() { |
| cancelFocusReleaseRequest(); |
| Message msg = obtainMessage(MSG_ANDROID_FOCUS_CHANGE); |
| sendMessage(msg); |
| } |
| |
| private void handleFocusReleaseRequest() { |
| if (DBG) { |
| Log.d(TAG_FOCUS, "handleFocusReleaseRequest"); |
| } |
| cancelFocusReleaseRequest(); |
| Message msg = obtainMessage(MSG_FOCUS_RELEASE); |
| sendMessageDelayed(msg, FOCUS_RELEASE_DELAY_MS); |
| } |
| |
| private void cancelFocusReleaseRequest() { |
| removeMessages(MSG_FOCUS_RELEASE); |
| } |
| |
| private void cancelAll() { |
| removeMessages(MSG_FOCUS_CHANGE); |
| removeMessages(MSG_STREAM_STATE_CHANGE); |
| removeMessages(MSG_ANDROID_FOCUS_CHANGE); |
| removeMessages(MSG_FOCUS_RELEASE); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_FOCUS_CHANGE: |
| doHandleCarFocusChange(); |
| break; |
| case MSG_STREAM_STATE_CHANGE: |
| doHandleStreamStatusChange(msg.arg1, msg.arg2 == 1); |
| break; |
| case MSG_ANDROID_FOCUS_CHANGE: |
| doHandleAndroidFocusChange(false /* triggeredByStreamChange */); |
| break; |
| case MSG_FOCUS_RELEASE: |
| doHandleFocusRelease(); |
| break; |
| } |
| } |
| } |
| |
| /** Wrapper class for holding the current focus state from car. */ |
| private static class FocusState { |
| public final int focusState; |
| public final int streams; |
| public final int externalFocus; |
| |
| private FocusState(int focusState, int streams, int externalFocus) { |
| this.focusState = focusState; |
| this.streams = streams; |
| this.externalFocus = externalFocus; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof FocusState)) { |
| return false; |
| } |
| FocusState that = (FocusState) o; |
| return this.focusState == that.focusState && this.streams == that.streams && |
| this.externalFocus == that.externalFocus; |
| } |
| |
| @Override |
| public String toString() { |
| return "FocusState, state:" + focusState + |
| " streams:0x" + Integer.toHexString(streams) + |
| " externalFocus:0x" + Integer.toHexString(externalFocus); |
| } |
| |
| public static FocusState create(int focusState, int streams, int externalAudios) { |
| return new FocusState(focusState, streams, externalAudios); |
| } |
| |
| public static FocusState create(int[] state) { |
| return create(state[AudioHalService.FOCUS_STATE_ARRAY_INDEX_STATE], |
| state[AudioHalService.FOCUS_STATE_ARRAY_INDEX_STREAMS], |
| state[AudioHalService.FOCUS_STATE_ARRAY_INDEX_EXTERNAL_FOCUS]); |
| } |
| |
| public static FocusState STATE_LOSS = |
| new FocusState(AudioHalService.VEHICLE_AUDIO_FOCUS_STATE_LOSS, 0, 0); |
| } |
| |
| /** Wrapper class for holding the focus requested to car. */ |
| private static class FocusRequest { |
| public final int focusRequest; |
| public final int streams; |
| public final int externalFocus; |
| |
| private FocusRequest(int focusRequest, int streams, int externalFocus) { |
| this.focusRequest = focusRequest; |
| this.streams = streams; |
| this.externalFocus = externalFocus; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof FocusRequest)) { |
| return false; |
| } |
| FocusRequest that = (FocusRequest) o; |
| return this.focusRequest == that.focusRequest && this.streams == that.streams && |
| this.externalFocus == that.externalFocus; |
| } |
| |
| @Override |
| public String toString() { |
| return "FocusRequest, request:" + focusRequest + |
| " streams:0x" + Integer.toHexString(streams) + |
| " externalFocus:0x" + Integer.toHexString(externalFocus); |
| } |
| |
| public static FocusRequest create(int focusRequest, int streams, int externalFocus) { |
| switch (focusRequest) { |
| case AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE: |
| return STATE_RELEASE; |
| } |
| return new FocusRequest(focusRequest, streams, externalFocus); |
| } |
| |
| public static FocusRequest STATE_RELEASE = |
| new FocusRequest(AudioHalService.VEHICLE_AUDIO_FOCUS_REQUEST_RELEASE, 0, 0); |
| } |
| |
| private static class ExtSourceInfo { |
| |
| public String source; |
| public int context; |
| |
| public ExtSourceInfo set(String source, int context) { |
| this.source = source; |
| this.context = context; |
| return this; |
| } |
| } |
| } |