/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.tv; import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED; import static android.media.tv.TvInputManager.INPUT_STATE_DISCONNECTED; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiHotplugEvent; import android.hardware.hdmi.IHdmiControlService; import android.hardware.hdmi.IHdmiDeviceEventListener; import android.hardware.hdmi.IHdmiHotplugEventListener; import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener; 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.AudioPort; import android.media.AudioPortConfig; import android.media.tv.ITvInputHardware; import android.media.tv.ITvInputHardwareCallback; import android.media.tv.TvInputHardwareInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvStreamConfig; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.KeyEvent; import android.view.Surface; import com.android.internal.os.SomeArgs; import com.android.server.SystemService; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * A helper class for TvInputManagerService to handle TV input hardware. * * This class does a basic connection management and forwarding calls to TvInputHal which eventually * calls to tv_input HAL module. * * @hide */ class TvInputHardwareManager implements TvInputHal.Callback { private static final String TAG = TvInputHardwareManager.class.getSimpleName(); private final Context mContext; private final Listener mListener; private final TvInputHal mHal = new TvInputHal(this); private final SparseArray mConnections = new SparseArray<>(); private final List mHardwareList = new ArrayList<>(); private final List mHdmiDeviceList = new LinkedList<>(); /* A map from a device ID to the matching TV input ID. */ private final SparseArray mHardwareInputIdMap = new SparseArray<>(); /* A map from a HDMI logical address to the matching TV input ID. */ private final SparseArray mHdmiInputIdMap = new SparseArray<>(); private final Map mInputMap = new ArrayMap<>(); private final AudioManager mAudioManager; private IHdmiControlService mHdmiControlService; private final IHdmiHotplugEventListener mHdmiHotplugEventListener = new HdmiHotplugEventListener(); private final IHdmiDeviceEventListener mHdmiDeviceEventListener = new HdmiDeviceEventListener(); private final IHdmiSystemAudioModeChangeListener mHdmiSystemAudioModeChangeListener = new HdmiSystemAudioModeChangeListener(); private final BroadcastReceiver mVolumeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { handleVolumeChange(context, intent); } }; private int mCurrentIndex = 0; private int mCurrentMaxIndex = 0; private final boolean mUseMasterVolume; // TODO: Should handle STANDBY case. private final SparseBooleanArray mHdmiStateMap = new SparseBooleanArray(); private final List mPendingHdmiDeviceEvents = new LinkedList<>(); // Calls to mListener should happen here. private final Handler mHandler = new ListenerHandler(); private final Object mLock = new Object(); public TvInputHardwareManager(Context context, Listener listener) { mContext = context; mListener = listener; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mUseMasterVolume = mContext.getResources().getBoolean( com.android.internal.R.bool.config_useMasterVolume); mHal.init(); } public void onBootPhase(int phase) { if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { mHdmiControlService = IHdmiControlService.Stub.asInterface(ServiceManager.getService( Context.HDMI_CONTROL_SERVICE)); if (mHdmiControlService != null) { try { mHdmiControlService.addHotplugEventListener(mHdmiHotplugEventListener); mHdmiControlService.addDeviceEventListener(mHdmiDeviceEventListener); mHdmiControlService.addSystemAudioModeChangeListener( mHdmiSystemAudioModeChangeListener); mHdmiDeviceList.addAll(mHdmiControlService.getInputDevices()); } catch (RemoteException e) { Slog.w(TAG, "Error registering listeners to HdmiControlService:", e); } } else { Slog.w(TAG, "HdmiControlService is not available"); } if (!mUseMasterVolume) { final IntentFilter filter = new IntentFilter(); filter.addAction(AudioManager.VOLUME_CHANGED_ACTION); filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION); mContext.registerReceiver(mVolumeReceiver, filter); } updateVolume(); } } @Override public void onDeviceAvailable(TvInputHardwareInfo info, TvStreamConfig[] configs) { synchronized (mLock) { Connection connection = new Connection(info); connection.updateConfigsLocked(configs); mConnections.put(info.getDeviceId(), connection); buildHardwareListLocked(); mHandler.obtainMessage( ListenerHandler.HARDWARE_DEVICE_ADDED, 0, 0, info).sendToTarget(); if (info.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) { processPendingHdmiDeviceEventsLocked(); } } } private void buildHardwareListLocked() { mHardwareList.clear(); for (int i = 0; i < mConnections.size(); ++i) { mHardwareList.add(mConnections.valueAt(i).getHardwareInfoLocked()); } } @Override public void onDeviceUnavailable(int deviceId) { synchronized (mLock) { Connection connection = mConnections.get(deviceId); if (connection == null) { Slog.e(TAG, "onDeviceUnavailable: Cannot find a connection with " + deviceId); return; } connection.resetLocked(null, null, null, null, null); mConnections.remove(deviceId); buildHardwareListLocked(); TvInputHardwareInfo info = connection.getHardwareInfoLocked(); if (info.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) { // Remove HDMI devices linked with this hardware. for (Iterator it = mHdmiDeviceList.iterator(); it.hasNext();) { HdmiDeviceInfo deviceInfo = it.next(); if (deviceInfo.getPortId() == info.getHdmiPortId()) { mHandler.obtainMessage(ListenerHandler.HDMI_DEVICE_REMOVED, 0, 0, deviceInfo).sendToTarget(); it.remove(); } } } mHandler.obtainMessage( ListenerHandler.HARDWARE_DEVICE_REMOVED, 0, 0, info).sendToTarget(); } } @Override public void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs) { synchronized (mLock) { Connection connection = mConnections.get(deviceId); if (connection == null) { Slog.e(TAG, "StreamConfigurationChanged: Cannot find a connection with " + deviceId); return; } connection.updateConfigsLocked(configs); String inputId = mHardwareInputIdMap.get(deviceId); if (inputId != null) { mHandler.obtainMessage(ListenerHandler.STATE_CHANGED, convertConnectedToState(configs.length > 0), 0, inputId).sendToTarget(); } ITvInputHardwareCallback callback = connection.getCallbackLocked(); if (callback != null) { try { callback.onStreamConfigChanged(configs); } catch (RemoteException e) { Slog.e(TAG, "error in onStreamConfigurationChanged", e); } } } } @Override public void onFirstFrameCaptured(int deviceId, int streamId) { synchronized (mLock) { Connection connection = mConnections.get(deviceId); if (connection == null) { Slog.e(TAG, "FirstFrameCaptured: Cannot find a connection with " + deviceId); return; } Runnable runnable = connection.getOnFirstFrameCapturedLocked(); if (runnable != null) { runnable.run(); connection.setOnFirstFrameCapturedLocked(null); } } } public List getHardwareList() { synchronized (mLock) { return Collections.unmodifiableList(mHardwareList); } } public List getHdmiDeviceList() { synchronized (mLock) { return Collections.unmodifiableList(mHdmiDeviceList); } } private boolean checkUidChangedLocked( Connection connection, int callingUid, int resolvedUserId) { Integer connectionCallingUid = connection.getCallingUidLocked(); Integer connectionResolvedUserId = connection.getResolvedUserIdLocked(); if (connectionCallingUid == null || connectionResolvedUserId == null) { return true; } if (connectionCallingUid != callingUid || connectionResolvedUserId != resolvedUserId) { return true; } return false; } private int convertConnectedToState(boolean connected) { if (connected) { return INPUT_STATE_CONNECTED; } else { return INPUT_STATE_DISCONNECTED; } } public void addHardwareTvInput(int deviceId, TvInputInfo info) { synchronized (mLock) { String oldInputId = mHardwareInputIdMap.get(deviceId); if (oldInputId != null) { Slog.w(TAG, "Trying to override previous registration: old = " + mInputMap.get(oldInputId) + ":" + deviceId + ", new = " + info + ":" + deviceId); } mHardwareInputIdMap.put(deviceId, info.getId()); mInputMap.put(info.getId(), info); // Process pending state changes // For logical HDMI devices, they have information from HDMI CEC signals. for (int i = 0; i < mHdmiStateMap.size(); ++i) { TvInputHardwareInfo hardwareInfo = findHardwareInfoForHdmiPortLocked(mHdmiStateMap.keyAt(i)); if (hardwareInfo == null) { continue; } String inputId = mHardwareInputIdMap.get(hardwareInfo.getDeviceId()); if (inputId != null && inputId.equals(info.getId())) { mHandler.obtainMessage(ListenerHandler.STATE_CHANGED, convertConnectedToState(mHdmiStateMap.valueAt(i)), 0, inputId).sendToTarget(); return; } } // For the rest of the devices, we can tell by the number of available streams. Connection connection = mConnections.get(deviceId); if (connection != null) { mHandler.obtainMessage(ListenerHandler.STATE_CHANGED, convertConnectedToState(connection.getConfigsLocked().length > 0), 0, info.getId()).sendToTarget(); return; } } } private static int indexOfEqualValue(SparseArray map, T value) { for (int i = 0; i < map.size(); ++i) { if (map.valueAt(i).equals(value)) { return i; } } return -1; } private static boolean intArrayContains(int[] array, int value) { for (int element : array) { if (element == value) return true; } return false; } public void addHdmiTvInput(int id, TvInputInfo info) { if (info.getType() != TvInputInfo.TYPE_HDMI) { throw new IllegalArgumentException("info (" + info + ") has non-HDMI type."); } synchronized (mLock) { String parentId = info.getParentId(); int parentIndex = indexOfEqualValue(mHardwareInputIdMap, parentId); if (parentIndex < 0) { throw new IllegalArgumentException("info (" + info + ") has invalid parentId."); } String oldInputId = mHdmiInputIdMap.get(id); if (oldInputId != null) { Slog.w(TAG, "Trying to override previous registration: old = " + mInputMap.get(oldInputId) + ":" + id + ", new = " + info + ":" + id); } mHdmiInputIdMap.put(id, info.getId()); mInputMap.put(info.getId(), info); } } public void removeTvInput(String inputId) { synchronized (mLock) { mInputMap.remove(inputId); int hardwareIndex = indexOfEqualValue(mHardwareInputIdMap, inputId); if (hardwareIndex >= 0) { mHardwareInputIdMap.removeAt(hardwareIndex); } int deviceIndex = indexOfEqualValue(mHdmiInputIdMap, inputId); if (deviceIndex >= 0) { mHdmiInputIdMap.removeAt(deviceIndex); } } } /** * Create a TvInputHardware object with a specific deviceId. One service at a time can access * the object, and if more than one process attempts to create hardware with the same deviceId, * the latest service will get the object and all the other hardware are released. The * release is notified via ITvInputHardwareCallback.onReleased(). */ public ITvInputHardware acquireHardware(int deviceId, ITvInputHardwareCallback callback, TvInputInfo info, int callingUid, int resolvedUserId) { if (callback == null) { throw new NullPointerException(); } synchronized (mLock) { Connection connection = mConnections.get(deviceId); if (connection == null) { Slog.e(TAG, "Invalid deviceId : " + deviceId); return null; } if (checkUidChangedLocked(connection, callingUid, resolvedUserId)) { TvInputHardwareImpl hardware = new TvInputHardwareImpl(connection.getHardwareInfoLocked()); try { callback.asBinder().linkToDeath(connection, 0); } catch (RemoteException e) { hardware.release(); return null; } connection.resetLocked(hardware, callback, info, callingUid, resolvedUserId); } return connection.getHardwareLocked(); } } /** * Release the specified hardware. */ public void releaseHardware(int deviceId, ITvInputHardware hardware, int callingUid, int resolvedUserId) { synchronized (mLock) { Connection connection = mConnections.get(deviceId); if (connection == null) { Slog.e(TAG, "Invalid deviceId : " + deviceId); return; } if (connection.getHardwareLocked() != hardware || checkUidChangedLocked(connection, callingUid, resolvedUserId)) { return; } connection.resetLocked(null, null, null, null, null); } } private TvInputHardwareInfo findHardwareInfoForHdmiPortLocked(int port) { for (TvInputHardwareInfo hardwareInfo : mHardwareList) { if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI && hardwareInfo.getHdmiPortId() == port) { return hardwareInfo; } } return null; } private int findDeviceIdForInputIdLocked(String inputId) { for (int i = 0; i < mConnections.size(); ++i) { Connection connection = mConnections.get(i); if (connection.getInfoLocked().getId().equals(inputId)) { return i; } } return -1; } /** * Get the list of TvStreamConfig which is buffered mode. */ public List getAvailableTvStreamConfigList(String inputId, int callingUid, int resolvedUserId) { List configsList = new ArrayList(); synchronized (mLock) { int deviceId = findDeviceIdForInputIdLocked(inputId); if (deviceId < 0) { Slog.e(TAG, "Invalid inputId : " + inputId); return configsList; } Connection connection = mConnections.get(deviceId); for (TvStreamConfig config : connection.getConfigsLocked()) { if (config.getType() == TvStreamConfig.STREAM_TYPE_BUFFER_PRODUCER) { configsList.add(config); } } } return configsList; } /** * Take a snapshot of the given TV input into the provided Surface. */ public boolean captureFrame(String inputId, Surface surface, final TvStreamConfig config, int callingUid, int resolvedUserId) { synchronized (mLock) { int deviceId = findDeviceIdForInputIdLocked(inputId); if (deviceId < 0) { Slog.e(TAG, "Invalid inputId : " + inputId); return false; } Connection connection = mConnections.get(deviceId); final TvInputHardwareImpl hardwareImpl = connection.getHardwareImplLocked(); if (hardwareImpl != null) { // Stop previous capture. Runnable runnable = connection.getOnFirstFrameCapturedLocked(); if (runnable != null) { runnable.run(); connection.setOnFirstFrameCapturedLocked(null); } boolean result = hardwareImpl.startCapture(surface, config); if (result) { connection.setOnFirstFrameCapturedLocked(new Runnable() { @Override public void run() { hardwareImpl.stopCapture(config); } }); } return result; } } return false; } private void processPendingHdmiDeviceEventsLocked() { for (Iterator it = mPendingHdmiDeviceEvents.iterator(); it.hasNext(); ) { Message msg = it.next(); HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj; TvInputHardwareInfo hardwareInfo = findHardwareInfoForHdmiPortLocked(deviceInfo.getPortId()); if (hardwareInfo != null) { msg.sendToTarget(); it.remove(); } } } private void updateVolume() { mCurrentMaxIndex = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); mCurrentIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); } private void handleVolumeChange(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); if (streamType != AudioManager.STREAM_MUSIC) { return; } int index = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); if (index == mCurrentIndex) { return; } mCurrentIndex = index; } else if (action.equals(AudioManager.STREAM_MUTE_CHANGED_ACTION)) { int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); if (streamType != AudioManager.STREAM_MUSIC) { return; } // volume index will be updated at onMediaStreamVolumeChanged() through updateVolume(). } else { Slog.w(TAG, "Unrecognized intent: " + intent); return; } synchronized (mLock) { for (int i = 0; i < mConnections.size(); ++i) { TvInputHardwareImpl hardwareImpl = mConnections.valueAt(i).getHardwareImplLocked(); if (hardwareImpl != null) { hardwareImpl.onMediaStreamVolumeChanged(); } } } } private float getMediaStreamVolume() { return mUseMasterVolume ? 1.0f : ((float) mCurrentIndex / (float) mCurrentMaxIndex); } private class Connection implements IBinder.DeathRecipient { private final TvInputHardwareInfo mHardwareInfo; private TvInputInfo mInfo; private TvInputHardwareImpl mHardware = null; private ITvInputHardwareCallback mCallback; private TvStreamConfig[] mConfigs = null; private Integer mCallingUid = null; private Integer mResolvedUserId = null; private Runnable mOnFirstFrameCaptured; public Connection(TvInputHardwareInfo hardwareInfo) { mHardwareInfo = hardwareInfo; } // *Locked methods assume TvInputHardwareManager.mLock is held. public void resetLocked(TvInputHardwareImpl hardware, ITvInputHardwareCallback callback, TvInputInfo info, Integer callingUid, Integer resolvedUserId) { if (mHardware != null) { try { mCallback.onReleased(); } catch (RemoteException e) { Slog.e(TAG, "error in Connection::resetLocked", e); } mHardware.release(); } mHardware = hardware; mCallback = callback; mInfo = info; mCallingUid = callingUid; mResolvedUserId = resolvedUserId; mOnFirstFrameCaptured = null; if (mHardware != null && mCallback != null) { try { mCallback.onStreamConfigChanged(getConfigsLocked()); } catch (RemoteException e) { Slog.e(TAG, "error in Connection::resetLocked", e); } } } public void updateConfigsLocked(TvStreamConfig[] configs) { mConfigs = configs; } public TvInputHardwareInfo getHardwareInfoLocked() { return mHardwareInfo; } public TvInputInfo getInfoLocked() { return mInfo; } public ITvInputHardware getHardwareLocked() { return mHardware; } public TvInputHardwareImpl getHardwareImplLocked() { return mHardware; } public ITvInputHardwareCallback getCallbackLocked() { return mCallback; } public TvStreamConfig[] getConfigsLocked() { return mConfigs; } public Integer getCallingUidLocked() { return mCallingUid; } public Integer getResolvedUserIdLocked() { return mResolvedUserId; } public void setOnFirstFrameCapturedLocked(Runnable runnable) { mOnFirstFrameCaptured = runnable; } public Runnable getOnFirstFrameCapturedLocked() { return mOnFirstFrameCaptured; } @Override public void binderDied() { synchronized (mLock) { resetLocked(null, null, null, null, null); } } } private class TvInputHardwareImpl extends ITvInputHardware.Stub { private final TvInputHardwareInfo mInfo; private boolean mReleased = false; private final Object mImplLock = new Object(); private final AudioManager.OnAudioPortUpdateListener mAudioListener = new AudioManager.OnAudioPortUpdateListener() { @Override public void onAudioPortListUpdate(AudioPort[] portList) { synchronized (mImplLock) { updateAudioConfigLocked(); } } @Override public void onAudioPatchListUpdate(AudioPatch[] patchList) { // No-op } @Override public void onServiceDied() { synchronized (mImplLock) { mAudioSource = null; mAudioSink.clear(); mAudioPatch = null; } } }; private int mOverrideAudioType = AudioManager.DEVICE_NONE; private String mOverrideAudioAddress = ""; private AudioDevicePort mAudioSource; private List mAudioSink = new ArrayList<>(); private AudioPatch mAudioPatch = null; // Set to an invalid value for a volume, so that current volume can be applied at the // first call to updateAudioConfigLocked(). private float mCommittedVolume = -1f; private float mSourceVolume = 0.0f; private TvStreamConfig mActiveConfig = null; private int mDesiredSamplingRate = 0; private int mDesiredChannelMask = AudioFormat.CHANNEL_OUT_DEFAULT; private int mDesiredFormat = AudioFormat.ENCODING_DEFAULT; public TvInputHardwareImpl(TvInputHardwareInfo info) { mInfo = info; mAudioManager.registerAudioPortUpdateListener(mAudioListener); if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) { mAudioSource = findAudioDevicePort(mInfo.getAudioType(), mInfo.getAudioAddress()); findAudioSinkFromAudioPolicy(mAudioSink); } } private void findAudioSinkFromAudioPolicy(List sinks) { sinks.clear(); ArrayList devicePorts = new ArrayList(); if (mAudioManager.listAudioDevicePorts(devicePorts) != AudioManager.SUCCESS) { return; } int sinkDevice = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC); for (AudioPort port : devicePorts) { AudioDevicePort devicePort = (AudioDevicePort) port; if ((devicePort.type() & sinkDevice) != 0) { sinks.add(devicePort); } } } private AudioDevicePort findAudioDevicePort(int type, String address) { if (type == AudioManager.DEVICE_NONE) { return null; } ArrayList devicePorts = new ArrayList(); if (mAudioManager.listAudioDevicePorts(devicePorts) != AudioManager.SUCCESS) { return null; } for (AudioPort port : devicePorts) { AudioDevicePort devicePort = (AudioDevicePort) port; if (devicePort.type() == type && devicePort.address().equals(address)) { return devicePort; } } return null; } public void release() { synchronized (mImplLock) { mAudioManager.unregisterAudioPortUpdateListener(mAudioListener); if (mAudioPatch != null) { mAudioManager.releaseAudioPatch(mAudioPatch); mAudioPatch = null; } mReleased = true; } } // A TvInputHardwareImpl object holds only one active session. Therefore, if a client // attempts to call setSurface with different TvStreamConfig objects, the last call will // prevail. @Override public boolean setSurface(Surface surface, TvStreamConfig config) throws RemoteException { synchronized (mImplLock) { if (mReleased) { throw new IllegalStateException("Device already released."); } int result = TvInputHal.SUCCESS; if (surface == null) { // The value of config is ignored when surface == null. if (mActiveConfig != null) { result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig); mActiveConfig = null; } else { // We already have no active stream. return true; } } else { // It's impossible to set a non-null surface with a null config. if (config == null) { return false; } // Remove stream only if we have an existing active configuration. if (mActiveConfig != null && !config.equals(mActiveConfig)) { result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig); if (result != TvInputHal.SUCCESS) { mActiveConfig = null; } } // Proceed only if all previous operations succeeded. if (result == TvInputHal.SUCCESS) { result = mHal.addOrUpdateStream(mInfo.getDeviceId(), surface, config); if (result == TvInputHal.SUCCESS) { mActiveConfig = config; } } } updateAudioConfigLocked(); return result == TvInputHal.SUCCESS; } } /** * Update audio configuration (source, sink, patch) all up to current state. */ private void updateAudioConfigLocked() { boolean sinkUpdated = updateAudioSinkLocked(); boolean sourceUpdated = updateAudioSourceLocked(); // We can't do updated = updateAudioSinkLocked() || updateAudioSourceLocked() here // because Java won't evaluate the latter if the former is true. if (mAudioSource == null || mAudioSink.isEmpty() || mActiveConfig == null) { if (mAudioPatch != null) { mAudioManager.releaseAudioPatch(mAudioPatch); mAudioPatch = null; } return; } updateVolume(); float volume = mSourceVolume * getMediaStreamVolume(); AudioGainConfig sourceGainConfig = null; if (mAudioSource.gains().length > 0 && volume != mCommittedVolume) { AudioGain sourceGain = null; for (AudioGain gain : mAudioSource.gains()) { if ((gain.mode() & AudioGain.MODE_JOINT) != 0) { sourceGain = gain; break; } } // NOTE: we only change the source gain in MODE_JOINT here. if (sourceGain != null) { int steps = (sourceGain.maxValue() - sourceGain.minValue()) / sourceGain.stepValue(); int gainValue = sourceGain.minValue(); if (volume < 1.0f) { gainValue += sourceGain.stepValue() * (int) (volume * steps + 0.5); } else { gainValue = sourceGain.maxValue(); } // size of gain values is 1 in MODE_JOINT int[] gainValues = new int[] { gainValue }; sourceGainConfig = sourceGain.buildConfig(AudioGain.MODE_JOINT, sourceGain.channelMask(), gainValues, 0); } else { Slog.w(TAG, "No audio source gain with MODE_JOINT support exists."); } } AudioPortConfig sourceConfig = mAudioSource.activeConfig(); List sinkConfigs = new ArrayList<>(); AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch }; boolean shouldRecreateAudioPatch = sourceUpdated || sinkUpdated; for (AudioDevicePort audioSink : mAudioSink) { AudioPortConfig sinkConfig = audioSink.activeConfig(); int sinkSamplingRate = mDesiredSamplingRate; int sinkChannelMask = mDesiredChannelMask; int sinkFormat = mDesiredFormat; // If sinkConfig != null and values are set to default, // fill in the sinkConfig values. if (sinkConfig != null) { if (sinkSamplingRate == 0) { sinkSamplingRate = sinkConfig.samplingRate(); } if (sinkChannelMask == AudioFormat.CHANNEL_OUT_DEFAULT) { sinkChannelMask = sinkConfig.channelMask(); } if (sinkFormat == AudioFormat.ENCODING_DEFAULT) { sinkChannelMask = sinkConfig.format(); } } if (sinkConfig == null || sinkConfig.samplingRate() != sinkSamplingRate || sinkConfig.channelMask() != sinkChannelMask || sinkConfig.format() != sinkFormat) { // Check for compatibility and reset to default if necessary. if (!intArrayContains(audioSink.samplingRates(), sinkSamplingRate) && audioSink.samplingRates().length > 0) { sinkSamplingRate = audioSink.samplingRates()[0]; } if (!intArrayContains(audioSink.channelMasks(), sinkChannelMask)) { sinkChannelMask = AudioFormat.CHANNEL_OUT_DEFAULT; } if (!intArrayContains(audioSink.formats(), sinkFormat)) { sinkFormat = AudioFormat.ENCODING_DEFAULT; } sinkConfig = audioSink.buildConfig(sinkSamplingRate, sinkChannelMask, sinkFormat, null); shouldRecreateAudioPatch = true; } sinkConfigs.add(sinkConfig); } // sinkConfigs.size() == mAudioSink.size(), and mAudioSink is guaranteed to be // non-empty at the beginning of this method. AudioPortConfig sinkConfig = sinkConfigs.get(0); if (sourceConfig == null || sourceGainConfig != null) { int sourceSamplingRate = 0; if (intArrayContains(mAudioSource.samplingRates(), sinkConfig.samplingRate())) { sourceSamplingRate = sinkConfig.samplingRate(); } else if (mAudioSource.samplingRates().length > 0) { // Use any sampling rate and hope audio patch can handle resampling... sourceSamplingRate = mAudioSource.samplingRates()[0]; } int sourceChannelMask = AudioFormat.CHANNEL_IN_DEFAULT; for (int inChannelMask : mAudioSource.channelMasks()) { if (AudioFormat.channelCountFromOutChannelMask(sinkConfig.channelMask()) == AudioFormat.channelCountFromInChannelMask(inChannelMask)) { sourceChannelMask = inChannelMask; break; } } int sourceFormat = AudioFormat.ENCODING_DEFAULT; if (intArrayContains(mAudioSource.formats(), sinkConfig.format())) { sourceFormat = sinkConfig.format(); } sourceConfig = mAudioSource.buildConfig(sourceSamplingRate, sourceChannelMask, sourceFormat, sourceGainConfig); shouldRecreateAudioPatch = true; } if (shouldRecreateAudioPatch) { mCommittedVolume = volume; mAudioManager.createAudioPatch( audioPatchArray, new AudioPortConfig[] { sourceConfig }, sinkConfigs.toArray(new AudioPortConfig[0])); mAudioPatch = audioPatchArray[0]; if (sourceGainConfig != null) { mAudioManager.setAudioPortGain(mAudioSource, sourceGainConfig); } } } @Override public void setStreamVolume(float volume) throws RemoteException { synchronized (mImplLock) { if (mReleased) { throw new IllegalStateException("Device already released."); } mSourceVolume = volume; updateAudioConfigLocked(); } } @Override public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException { synchronized (mImplLock) { if (mReleased) { throw new IllegalStateException("Device already released."); } } if (mInfo.getType() != TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) { return false; } // TODO(hdmi): mHdmiClient.sendKeyEvent(event); return false; } private boolean startCapture(Surface surface, TvStreamConfig config) { synchronized (mImplLock) { if (mReleased) { return false; } if (surface == null || config == null) { return false; } if (config.getType() != TvStreamConfig.STREAM_TYPE_BUFFER_PRODUCER) { return false; } int result = mHal.addOrUpdateStream(mInfo.getDeviceId(), surface, config); return result == TvInputHal.SUCCESS; } } private boolean stopCapture(TvStreamConfig config) { synchronized (mImplLock) { if (mReleased) { return false; } if (config == null) { return false; } int result = mHal.removeStream(mInfo.getDeviceId(), config); return result == TvInputHal.SUCCESS; } } private boolean updateAudioSourceLocked() { if (mInfo.getAudioType() == AudioManager.DEVICE_NONE) { return false; } AudioDevicePort previousSource = mAudioSource; mAudioSource = findAudioDevicePort(mInfo.getAudioType(), mInfo.getAudioAddress()); return mAudioSource == null ? (previousSource != null) : !mAudioSource.equals(previousSource); } private boolean updateAudioSinkLocked() { if (mInfo.getAudioType() == AudioManager.DEVICE_NONE) { return false; } List previousSink = mAudioSink; mAudioSink = new ArrayList<>(); if (mOverrideAudioType == AudioManager.DEVICE_NONE) { findAudioSinkFromAudioPolicy(mAudioSink); } else { AudioDevicePort audioSink = findAudioDevicePort(mOverrideAudioType, mOverrideAudioAddress); if (audioSink != null) { mAudioSink.add(audioSink); } } // Returns true if mAudioSink and previousSink differs. if (mAudioSink.size() != previousSink.size()) { return true; } previousSink.removeAll(mAudioSink); return !previousSink.isEmpty(); } private void handleAudioSinkUpdated() { synchronized (mImplLock) { updateAudioConfigLocked(); } } @Override public void overrideAudioSink(int audioType, String audioAddress, int samplingRate, int channelMask, int format) { synchronized (mImplLock) { mOverrideAudioType = audioType; mOverrideAudioAddress = audioAddress; mDesiredSamplingRate = samplingRate; mDesiredChannelMask = channelMask; mDesiredFormat = format; updateAudioConfigLocked(); } } public void onMediaStreamVolumeChanged() { synchronized (mImplLock) { updateAudioConfigLocked(); } } } interface Listener { public void onStateChanged(String inputId, int state); public void onHardwareDeviceAdded(TvInputHardwareInfo info); public void onHardwareDeviceRemoved(TvInputHardwareInfo info); public void onHdmiDeviceAdded(HdmiDeviceInfo device); public void onHdmiDeviceRemoved(HdmiDeviceInfo device); public void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo device); } private class ListenerHandler extends Handler { private static final int STATE_CHANGED = 1; private static final int HARDWARE_DEVICE_ADDED = 2; private static final int HARDWARE_DEVICE_REMOVED = 3; private static final int HDMI_DEVICE_ADDED = 4; private static final int HDMI_DEVICE_REMOVED = 5; private static final int HDMI_DEVICE_UPDATED = 6; @Override public final void handleMessage(Message msg) { switch (msg.what) { case STATE_CHANGED: { String inputId = (String) msg.obj; int state = msg.arg1; mListener.onStateChanged(inputId, state); break; } case HARDWARE_DEVICE_ADDED: { TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj; mListener.onHardwareDeviceAdded(info); break; } case HARDWARE_DEVICE_REMOVED: { TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj; mListener.onHardwareDeviceRemoved(info); break; } case HDMI_DEVICE_ADDED: { HdmiDeviceInfo info = (HdmiDeviceInfo) msg.obj; mListener.onHdmiDeviceAdded(info); break; } case HDMI_DEVICE_REMOVED: { HdmiDeviceInfo info = (HdmiDeviceInfo) msg.obj; mListener.onHdmiDeviceRemoved(info); break; } case HDMI_DEVICE_UPDATED: { HdmiDeviceInfo info = (HdmiDeviceInfo) msg.obj; String inputId = null; synchronized (mLock) { inputId = mHdmiInputIdMap.get(info.getId()); } if (inputId != null) { mListener.onHdmiDeviceUpdated(inputId, info); } else { Slog.w(TAG, "Could not resolve input ID matching the device info; " + "ignoring."); } break; } default: { Slog.w(TAG, "Unhandled message: " + msg); break; } } } } // Listener implementations for HdmiControlService private final class HdmiHotplugEventListener extends IHdmiHotplugEventListener.Stub { @Override public void onReceived(HdmiHotplugEvent event) { synchronized (mLock) { mHdmiStateMap.put(event.getPort(), event.isConnected()); TvInputHardwareInfo hardwareInfo = findHardwareInfoForHdmiPortLocked(event.getPort()); if (hardwareInfo == null) { return; } String inputId = mHardwareInputIdMap.get(hardwareInfo.getDeviceId()); if (inputId == null) { return; } mHandler.obtainMessage(ListenerHandler.STATE_CHANGED, convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget(); } } } private final class HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub { @Override public void onStatusChanged(HdmiDeviceInfo deviceInfo, int status) { if (!deviceInfo.isSourceType()) return; synchronized (mLock) { int messageType = 0; Object obj = null; switch (status) { case HdmiControlManager.DEVICE_EVENT_ADD_DEVICE: { if (findHdmiDeviceInfo(deviceInfo.getId()) == null) { mHdmiDeviceList.add(deviceInfo); } else { Slog.w(TAG, "The list already contains " + deviceInfo + "; ignoring."); return; } messageType = ListenerHandler.HDMI_DEVICE_ADDED; obj = deviceInfo; break; } case HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE: { HdmiDeviceInfo originalDeviceInfo = findHdmiDeviceInfo(deviceInfo.getId()); if (!mHdmiDeviceList.remove(originalDeviceInfo)) { Slog.w(TAG, "The list doesn't contain " + deviceInfo + "; ignoring."); return; } messageType = ListenerHandler.HDMI_DEVICE_REMOVED; obj = deviceInfo; break; } case HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE: { HdmiDeviceInfo originalDeviceInfo = findHdmiDeviceInfo(deviceInfo.getId()); if (!mHdmiDeviceList.remove(originalDeviceInfo)) { Slog.w(TAG, "The list doesn't contain " + deviceInfo + "; ignoring."); return; } mHdmiDeviceList.add(deviceInfo); messageType = ListenerHandler.HDMI_DEVICE_UPDATED; obj = deviceInfo; break; } } Message msg = mHandler.obtainMessage(messageType, 0, 0, obj); if (findHardwareInfoForHdmiPortLocked(deviceInfo.getPortId()) != null) { msg.sendToTarget(); } else { mPendingHdmiDeviceEvents.add(msg); } } } private HdmiDeviceInfo findHdmiDeviceInfo(int id) { for (HdmiDeviceInfo info : mHdmiDeviceList) { if (info.getId() == id) { return info; } } return null; } } private final class HdmiSystemAudioModeChangeListener extends IHdmiSystemAudioModeChangeListener.Stub { @Override public void onStatusChanged(boolean enabled) throws RemoteException { synchronized (mLock) { for (int i = 0; i < mConnections.size(); ++i) { TvInputHardwareImpl impl = mConnections.valueAt(i).getHardwareImplLocked(); if (impl != null) { impl.handleAudioSinkUpdated(); } } } } } }