/*
* 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.server.SystemService;
import java.util.ArrayList;
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<Connection> mConnections = new SparseArray<>();
private final List<TvInputHardwareInfo> mHardwareList = new ArrayList<>();
private final List<HdmiDeviceInfo> mHdmiDeviceList = new LinkedList<>();
/* A map from a device ID to the matching TV input ID. */
private final SparseArray<String> mHardwareInputIdMap = new SparseArray<>();
/* A map from a HDMI logical address to the matching TV input ID. */
private final SparseArray<String> mHdmiInputIdMap = new SparseArray<>();
private final Map<String, TvInputInfo> mInputMap = new ArrayMap<>();
private final AudioManager mAudioManager;
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;
// TODO: Should handle STANDBY case.
private final SparseBooleanArray mHdmiStateMap = new SparseBooleanArray();
private final List<Message> 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);
mHal.init();
}
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
IHdmiControlService hdmiControlService = IHdmiControlService.Stub.asInterface(
ServiceManager.getService(Context.HDMI_CONTROL_SERVICE));
if (hdmiControlService != null) {
try {
hdmiControlService.addHotplugEventListener(mHdmiHotplugEventListener);
hdmiControlService.addDeviceEventListener(mHdmiDeviceEventListener);
hdmiControlService.addSystemAudioModeChangeListener(
mHdmiSystemAudioModeChangeListener);
mHdmiDeviceList.addAll(hdmiControlService.getInputDevices());
} catch (RemoteException e) {
Slog.w(TAG, "Error registering listeners to HdmiControlService:", e);
}
} else {
Slog.w(TAG, "HdmiControlService is not available");
}
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<HdmiDeviceInfo> 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<TvInputHardwareInfo> getHardwareList() {
synchronized (mLock) {
return Collections.unmodifiableList(mHardwareList);
}
}
public List<HdmiDeviceInfo> getHdmiDeviceList() {
synchronized (mLock) {
return Collections.unmodifiableList(mHdmiDeviceList);
}
}
private boolean checkUidChangedLocked(
Connection connection, int callingUid, int resolvedUserId) {
Integer connectionCallingUid = connection.getCallingUidLocked();
Integer connectionResolvedUserId = connection.getResolvedUserIdLocked();
return connectionCallingUid == null || connectionResolvedUserId == null
|| connectionCallingUid != callingUid || connectionResolvedUserId != resolvedUserId;
}
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();
}
}
}
private static <T> int indexOfEqualValue(SparseArray<T> 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<TvStreamConfig> getAvailableTvStreamConfigList(String inputId, int callingUid,
int resolvedUserId) {
List<TvStreamConfig> 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<Message> 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();
switch (action) {
case 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;
break;
}
case 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().
break;
}
default:
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 (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<AudioDevicePort> 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<AudioDevicePort> sinks) {
sinks.clear();
ArrayList<AudioDevicePort> devicePorts = new ArrayList<>();
if (mAudioManager.listAudioDevicePorts(devicePorts) != AudioManager.SUCCESS) {
return;
}
int sinkDevice = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
for (AudioDevicePort port : devicePorts) {
if ((port.type() & sinkDevice) != 0) {
sinks.add(port);
}
}
}
private AudioDevicePort findAudioDevicePort(int type, String address) {
if (type == AudioManager.DEVICE_NONE) {
return null;
}
ArrayList<AudioDevicePort> devicePorts = new ArrayList<>();
if (mAudioManager.listAudioDevicePorts(devicePorts) != AudioManager.SUCCESS) {
return null;
}
for (AudioDevicePort port : devicePorts) {
if (port.type() == type && port.address().equals(address)) {
return port;
}
}
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<AudioPortConfig> 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[sinkConfigs.size()]));
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<AudioDevicePort> 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 {
void onStateChanged(String inputId, int state);
void onHardwareDeviceAdded(TvInputHardwareInfo info);
void onHardwareDeviceRemoved(TvInputHardwareInfo info);
void onHdmiDeviceAdded(HdmiDeviceInfo device);
void onHdmiDeviceRemoved(HdmiDeviceInfo device);
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;
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();
}
}
}
}
}
}