| /* |
| * 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<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 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<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); |
| 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<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(); |
| 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 <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<TvStreamConfig>(); |
| 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(); |
| 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<AudioDevicePort> mAudioSink = new ArrayList<>(); |
| private AudioPatch mAudioPatch = null; |
| private float mCommittedVolume = 0.0f; |
| 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<AudioPort> devicePorts = new ArrayList<AudioPort>(); |
| 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<AudioPort> devicePorts = new ArrayList<AudioPort>(); |
| 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<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[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<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 { |
| 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(); |
| } |
| } |
| } |
| } |
| } |
| } |