blob: 252f2f4373cb2e2f605abe3b56bce4e1ff12698f [file] [log] [blame]
/*
* 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.Context;
import android.hardware.hdmi.HdmiCecDeviceInfo;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiHotplugEvent;
import android.hardware.hdmi.IHdmiDeviceEventListener;
import android.media.AudioDevicePort;
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.Looper;
import android.os.Message;
import android.os.RemoteException;
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.HashSet;
import java.util.List;
import java.util.Set;
/**
* 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, HdmiControlManager.HotplugEventListener {
private static final String TAG = TvInputHardwareManager.class.getSimpleName();
private final TvInputHal mHal = new TvInputHal(this);
private final SparseArray<Connection> mConnections = new SparseArray<Connection>();
private final List<TvInputHardwareInfo> mInfoList = new ArrayList<TvInputHardwareInfo>();
private final Context mContext;
private final Listener mListener;
private final Set<Integer> mActiveHdmiSources = new HashSet<Integer>();
private final AudioManager mAudioManager;
private final SparseBooleanArray mHdmiStateMap = new SparseBooleanArray();
// TODO: Should handle INACTIVE case.
private final SparseArray<TvInputInfo> mTvInputInfoMap = new SparseArray<TvInputInfo>();
private final IHdmiDeviceEventListener mHdmiDeviceEventListener = new HdmiDeviceEventListener();
// 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) {
HdmiControlManager hdmiControlManager =
(HdmiControlManager) mContext.getSystemService(Context.HDMI_CONTROL_SERVICE);
hdmiControlManager.addHotplugEventListener(this);
}
}
@Override
public void onDeviceAvailable(
TvInputHardwareInfo info, TvStreamConfig[] configs) {
synchronized (mLock) {
Connection connection = new Connection(info);
connection.updateConfigsLocked(configs);
mConnections.put(info.getDeviceId(), connection);
buildInfoListLocked();
mHandler.obtainMessage(
ListenerHandler.HARDWARE_DEVICE_ADDED, 0, 0, info).sendToTarget();
}
}
private void buildInfoListLocked() {
mInfoList.clear();
for (int i = 0; i < mConnections.size(); ++i) {
mInfoList.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);
buildInfoListLocked();
mHandler.obtainMessage(
ListenerHandler.HARDWARE_DEVICE_REMOVED, deviceId, 0).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);
try {
connection.getCallbackLocked().onStreamConfigChanged(configs);
} catch (RemoteException e) {
Slog.e(TAG, "onStreamConfigurationChanged: " + e);
}
}
}
public List<TvInputHardwareInfo> getHardwareList() {
synchronized (mLock) {
return mInfoList;
}
}
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 registerTvInputInfo(TvInputInfo info, int deviceId) {
if (info.getType() == TvInputInfo.TYPE_VIRTUAL) {
throw new IllegalArgumentException("info (" + info + ") has virtual type.");
}
synchronized (mLock) {
if (mTvInputInfoMap.indexOfKey(deviceId) >= 0) {
Slog.w(TAG, "Trying to override previous registration: old = "
+ mTvInputInfoMap.get(deviceId) + ":" + deviceId + ", new = "
+ info + ":" + deviceId);
}
mTvInputInfoMap.put(deviceId, info);
for (int i = 0; i < mHdmiStateMap.size(); ++i) {
String inputId = findInputIdForHdmiPortLocked(mHdmiStateMap.keyAt(i));
if (inputId != null && inputId.equals(info.getId())) {
mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
convertConnectedToState(mHdmiStateMap.valueAt(i)), 0,
inputId).sendToTarget();
}
}
}
}
/**
* 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 String findInputIdForHdmiPortLocked(int port) {
for (TvInputHardwareInfo hardwareInfo : mInfoList) {
if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI
&& hardwareInfo.getHdmiPortId() == port) {
TvInputInfo info = mTvInputInfoMap.get(hardwareInfo.getDeviceId());
return (info == null) ? null : info.getId();
}
}
return null;
}
// HdmiControlManager.HotplugEventListener implementation.
@Override
public void onReceived(HdmiHotplugEvent event) {
String inputId = null;
synchronized (mLock) {
mHdmiStateMap.put(event.getPort(), event.isConnected());
inputId = findInputIdForHdmiPortLocked(event.getPort());
if (inputId == null) {
return;
}
mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget();
}
}
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;
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, "Connection::resetHardware: " + e);
}
mHardware.release();
}
mHardware = hardware;
mCallback = callback;
mInfo = info;
mCallingUid = callingUid;
mResolvedUserId = resolvedUserId;
if (mHardware != null && mCallback != null) {
try {
mCallback.onStreamConfigChanged(getConfigsLocked());
} catch (RemoteException e) {
Slog.e(TAG, "Connection::resetHardware: " + e);
}
}
}
public void updateConfigsLocked(TvStreamConfig[] configs) {
mConfigs = configs;
}
public TvInputHardwareInfo getHardwareInfoLocked() {
return mHardwareInfo;
}
public TvInputInfo getInfoLocked() {
return mInfo;
}
public ITvInputHardware getHardwareLocked() {
return mHardware;
}
public ITvInputHardwareCallback getCallbackLocked() {
return mCallback;
}
public TvStreamConfig[] getConfigsLocked() {
return mConfigs;
}
public Integer getCallingUidLocked() {
return mCallingUid;
}
public Integer getResolvedUserIdLocked() {
return mResolvedUserId;
}
@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 AudioDevicePort mAudioSource;
private final AudioDevicePort mAudioSink;
private AudioPatch mAudioPatch = null;
private TvStreamConfig mActiveConfig = null;
public TvInputHardwareImpl(TvInputHardwareInfo info) {
mInfo = info;
AudioDevicePort audioSource = null;
AudioDevicePort audioSink = null;
if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) {
ArrayList<AudioPort> devicePorts = new ArrayList<AudioPort>();
if (mAudioManager.listAudioDevicePorts(devicePorts) == AudioManager.SUCCESS) {
// Find source
for (AudioPort port : devicePorts) {
AudioDevicePort devicePort = (AudioDevicePort) port;
if (devicePort.type() == mInfo.getAudioType() &&
devicePort.address().equals(mInfo.getAudioAddress())) {
audioSource = devicePort;
break;
}
}
// Find sink
// TODO: App may want to specify sink device?
int sinkDevices = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
for (AudioPort port : devicePorts) {
AudioDevicePort devicePort = (AudioDevicePort) port;
if (devicePort.type() == sinkDevices) {
audioSink = devicePort;
break;
}
}
}
}
mAudioSource = audioSource;
mAudioSink = audioSink;
}
public void release() {
synchronized (mImplLock) {
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.");
}
if (surface != null && config == null) {
return false;
}
if (surface == null && mActiveConfig == null) {
return false;
}
if (mInfo.getType() == TvInputHal.TYPE_HDMI) {
if (surface != null) {
// Set "Active Source" for HDMI.
// TODO(hdmi): mHdmiClient.deviceSelect(...);
mActiveHdmiSources.add(mInfo.getDeviceId());
} else {
mActiveHdmiSources.remove(mInfo.getDeviceId());
if (mActiveHdmiSources.size() == 0) {
// Tell HDMI that no HDMI source is active
// TODO(hdmi): mHdmiClient.portSelect(null);
}
}
}
if (mAudioSource != null && mAudioSink != null) {
if (surface != null) {
AudioPortConfig sourceConfig = mAudioSource.activeConfig();
AudioPortConfig sinkConfig = mAudioSink.activeConfig();
AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch };
// TODO: build config if activeConfig() == null
mAudioManager.createAudioPatch(
audioPatchArray,
new AudioPortConfig[] { sourceConfig },
new AudioPortConfig[] { sinkConfig });
mAudioPatch = audioPatchArray[0];
} else {
mAudioManager.releaseAudioPatch(mAudioPatch);
mAudioPatch = null;
}
}
int result = TvInputHal.ERROR_UNKNOWN;
if (surface == null) {
result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
mActiveConfig = null;
} else {
if (config != mActiveConfig && mActiveConfig != null) {
result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
if (result != TvInputHal.SUCCESS) {
mActiveConfig = null;
return false;
}
}
result = mHal.addStream(mInfo.getDeviceId(), surface, config);
if (result == TvInputHal.SUCCESS) {
mActiveConfig = config;
}
}
return result == TvInputHal.SUCCESS;
}
}
@Override
public void setVolume(float volume) throws RemoteException {
synchronized (mImplLock) {
if (mReleased) {
throw new IllegalStateException("Device already released.");
}
}
// TODO: Use AudioGain?
}
@Override
public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException {
synchronized (mImplLock) {
if (mReleased) {
throw new IllegalStateException("Device already released.");
}
}
if (mInfo.getType() != TvInputHal.TYPE_HDMI) {
return false;
}
// TODO(hdmi): mHdmiClient.sendKeyEvent(event);
return false;
}
}
interface Listener {
public void onStateChanged(String inputId, int state);
public void onHardwareDeviceAdded(TvInputHardwareInfo info);
public void onHardwareDeviceRemoved(int deviceId);
public void onHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDevice);
public void onHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDevice);
}
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 CEC_DEVICE_ADDED = 4;
private static final int CEC_DEVICE_REMOVED = 5;
@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: {
int deviceId = msg.arg1;
mListener.onHardwareDeviceRemoved(deviceId);
break;
}
case CEC_DEVICE_ADDED: {
HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
mListener.onHdmiCecDeviceAdded(info);
break;
}
case CEC_DEVICE_REMOVED: {
HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
mListener.onHdmiCecDeviceRemoved(info);
break;
}
default: {
Slog.w(TAG, "Unhandled message: " + msg);
break;
}
}
}
}
private final class HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub {
@Override
public void onStatusChanged(HdmiCecDeviceInfo deviceInfo, boolean activated) {
mHandler.obtainMessage(
activated ? ListenerHandler.CEC_DEVICE_ADDED
: ListenerHandler.CEC_DEVICE_REMOVED,
0, 0, deviceInfo).sendToTarget();
}
}
}