TvInputHardwareManager.java revision 0f68d62a4b10aff3c99162bf3a8322d38e643088
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.tv;
18
19import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
20import static android.media.tv.TvInputManager.INPUT_STATE_DISCONNECTED;
21
22import android.content.Context;
23import android.content.Intent;
24import android.hardware.hdmi.HdmiCecDeviceInfo;
25import android.hardware.hdmi.HdmiHotplugEvent;
26import android.hardware.hdmi.IHdmiControlService;
27import android.hardware.hdmi.IHdmiDeviceEventListener;
28import android.hardware.hdmi.IHdmiHotplugEventListener;
29import android.hardware.hdmi.IHdmiInputChangeListener;
30import android.media.AudioDevicePort;
31import android.media.AudioManager;
32import android.media.AudioPatch;
33import android.media.AudioPort;
34import android.media.AudioPortConfig;
35import android.media.tv.ITvInputHardware;
36import android.media.tv.ITvInputHardwareCallback;
37import android.media.tv.TvInputHardwareInfo;
38import android.media.tv.TvContract;
39import android.media.tv.TvInputInfo;
40import android.media.tv.TvStreamConfig;
41import android.os.Handler;
42import android.os.IBinder;
43import android.os.Looper;
44import android.os.Message;
45import android.os.RemoteException;
46import android.os.ServiceManager;
47import android.util.ArrayMap;
48import android.util.Slog;
49import android.util.SparseArray;
50import android.util.SparseBooleanArray;
51import android.view.KeyEvent;
52import android.view.Surface;
53
54import com.android.server.SystemService;
55
56import java.util.ArrayList;
57import java.util.Collections;
58import java.util.HashSet;
59import java.util.List;
60import java.util.Map;
61import java.util.Set;
62
63/**
64 * A helper class for TvInputManagerService to handle TV input hardware.
65 *
66 * This class does a basic connection management and forwarding calls to TvInputHal which eventually
67 * calls to tv_input HAL module.
68 *
69 * @hide
70 */
71class TvInputHardwareManager implements TvInputHal.Callback {
72    private static final String TAG = TvInputHardwareManager.class.getSimpleName();
73
74    private final Context mContext;
75    private final Listener mListener;
76    private final TvInputHal mHal = new TvInputHal(this);
77    private final SparseArray<Connection> mConnections = new SparseArray<Connection>();
78    private final List<TvInputHardwareInfo> mInfoList = new ArrayList<TvInputHardwareInfo>();
79    /* A map from a device ID to the matching TV input ID. */
80    private final SparseArray<String> mHardwareInputIdMap = new SparseArray<String>();
81    /* A map from a HDMI logical address to the matching TV input ID. */
82    private final SparseArray<String> mHdmiCecInputIdMap = new SparseArray<String>();
83    private final Map<String, TvInputInfo> mInputMap = new ArrayMap<String, TvInputInfo>();
84
85    private final AudioManager mAudioManager;
86    private IHdmiControlService mHdmiControlService;
87    private final IHdmiHotplugEventListener mHdmiHotplugEventListener =
88            new HdmiHotplugEventListener();
89    private final IHdmiDeviceEventListener mHdmiDeviceEventListener = new HdmiDeviceEventListener();
90    private final IHdmiInputChangeListener mHdmiInputChangeListener = new HdmiInputChangeListener();
91    private final Set<Integer> mActiveHdmiSources = new HashSet<Integer>();
92    // TODO: Should handle INACTIVE case.
93    private final SparseBooleanArray mHdmiStateMap = new SparseBooleanArray();
94
95    // Calls to mListener should happen here.
96    private final Handler mHandler = new ListenerHandler();
97
98    private final Object mLock = new Object();
99
100    public TvInputHardwareManager(Context context, Listener listener) {
101        mContext = context;
102        mListener = listener;
103        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
104        mHal.init();
105    }
106
107    public void onBootPhase(int phase) {
108        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
109            mHdmiControlService = IHdmiControlService.Stub.asInterface(ServiceManager.getService(
110                    Context.HDMI_CONTROL_SERVICE));
111            if (mHdmiControlService != null) {
112                try {
113                    mHdmiControlService.addHotplugEventListener(mHdmiHotplugEventListener);
114                    mHdmiControlService.addDeviceEventListener(mHdmiDeviceEventListener);
115                    mHdmiControlService.setInputChangeListener(mHdmiInputChangeListener);
116                } catch (RemoteException e) {
117                    Slog.w(TAG, "Error registering listeners to HdmiControlService:", e);
118                }
119            }
120        }
121    }
122
123    @Override
124    public void onDeviceAvailable(TvInputHardwareInfo info, TvStreamConfig[] configs) {
125        synchronized (mLock) {
126            Connection connection = new Connection(info);
127            connection.updateConfigsLocked(configs);
128            mConnections.put(info.getDeviceId(), connection);
129            buildInfoListLocked();
130            mHandler.obtainMessage(
131                    ListenerHandler.HARDWARE_DEVICE_ADDED, 0, 0, info).sendToTarget();
132        }
133    }
134
135    private void buildInfoListLocked() {
136        mInfoList.clear();
137        for (int i = 0; i < mConnections.size(); ++i) {
138            mInfoList.add(mConnections.valueAt(i).getHardwareInfoLocked());
139        }
140    }
141
142    @Override
143    public void onDeviceUnavailable(int deviceId) {
144        synchronized (mLock) {
145            Connection connection = mConnections.get(deviceId);
146            if (connection == null) {
147                Slog.e(TAG, "onDeviceUnavailable: Cannot find a connection with " + deviceId);
148                return;
149            }
150            connection.resetLocked(null, null, null, null, null);
151            mConnections.remove(deviceId);
152            buildInfoListLocked();
153            TvInputHardwareInfo info = connection.getHardwareInfoLocked();
154            mHandler.obtainMessage(
155                    ListenerHandler.HARDWARE_DEVICE_REMOVED, 0, 0, info).sendToTarget();
156        }
157    }
158
159    @Override
160    public void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs) {
161        synchronized (mLock) {
162            Connection connection = mConnections.get(deviceId);
163            if (connection == null) {
164                Slog.e(TAG, "StreamConfigurationChanged: Cannot find a connection with "
165                        + deviceId);
166                return;
167            }
168            connection.updateConfigsLocked(configs);
169            try {
170                connection.getCallbackLocked().onStreamConfigChanged(configs);
171            } catch (RemoteException e) {
172                Slog.e(TAG, "error in onStreamConfigurationChanged", e);
173            }
174        }
175    }
176
177    public List<TvInputHardwareInfo> getHardwareList() {
178        synchronized (mLock) {
179            return mInfoList;
180        }
181    }
182
183    public List<HdmiCecDeviceInfo> getHdmiCecInputDeviceList() {
184        if (mHdmiControlService != null) {
185            try {
186                return mHdmiControlService.getInputDevices();
187            } catch (RemoteException e) {
188                Slog.e(TAG, "error in getHdmiCecInputDeviceList", e);
189            }
190        }
191        return Collections.emptyList();
192    }
193
194    private boolean checkUidChangedLocked(
195            Connection connection, int callingUid, int resolvedUserId) {
196        Integer connectionCallingUid = connection.getCallingUidLocked();
197        Integer connectionResolvedUserId = connection.getResolvedUserIdLocked();
198        if (connectionCallingUid == null || connectionResolvedUserId == null) {
199            return true;
200        }
201        if (connectionCallingUid != callingUid || connectionResolvedUserId != resolvedUserId) {
202            return true;
203        }
204        return false;
205    }
206
207    private int convertConnectedToState(boolean connected) {
208        if (connected) {
209            return INPUT_STATE_CONNECTED;
210        } else {
211            return INPUT_STATE_DISCONNECTED;
212        }
213    }
214
215    public void addHardwareTvInput(int deviceId, TvInputInfo info) {
216        if (info.getType() == TvInputInfo.TYPE_VIRTUAL) {
217            throw new IllegalArgumentException("info (" + info + ") has virtual type.");
218        }
219        synchronized (mLock) {
220            String oldInputId = mHardwareInputIdMap.get(deviceId);
221            if (oldInputId != null) {
222                Slog.w(TAG, "Trying to override previous registration: old = "
223                        + mInputMap.get(oldInputId) + ":" + deviceId + ", new = "
224                        + info + ":" + deviceId);
225            }
226            mHardwareInputIdMap.put(deviceId, info.getId());
227            mInputMap.put(info.getId(), info);
228
229            for (int i = 0; i < mHdmiStateMap.size(); ++i) {
230                String inputId = findInputIdForHdmiPortLocked(mHdmiStateMap.keyAt(i));
231                if (inputId != null && inputId.equals(info.getId())) {
232                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
233                            convertConnectedToState(mHdmiStateMap.valueAt(i)), 0,
234                            inputId).sendToTarget();
235                }
236            }
237        }
238    }
239
240    private static <T> int indexOfEqualValue(SparseArray<T> map, T value) {
241        for (int i = 0; i < map.size(); ++i) {
242            if (map.valueAt(i).equals(value)) {
243                return i;
244            }
245        }
246        return -1;
247    }
248
249    public void addHdmiCecTvInput(int logicalAddress, TvInputInfo info) {
250        if (info.getType() != TvInputInfo.TYPE_HDMI) {
251            throw new IllegalArgumentException("info (" + info + ") has non-HDMI type.");
252        }
253        synchronized (mLock) {
254            String parentId = info.getParentId();
255            int parentIndex = indexOfEqualValue(mHardwareInputIdMap, parentId);
256            if (parentIndex < 0) {
257                throw new IllegalArgumentException("info (" + info + ") has invalid parentId.");
258            }
259            String oldInputId = mHdmiCecInputIdMap.get(logicalAddress);
260            if (oldInputId != null) {
261                Slog.w(TAG, "Trying to override previous registration: old = "
262                        + mInputMap.get(oldInputId) + ":" + logicalAddress + ", new = "
263                        + info + ":" + logicalAddress);
264            }
265            mHdmiCecInputIdMap.put(logicalAddress, info.getId());
266            mInputMap.put(info.getId(), info);
267        }
268    }
269
270    public void removeTvInput(String inputId) {
271        synchronized (mLock) {
272            mInputMap.remove(inputId);
273            int hardwareIndex = indexOfEqualValue(mHardwareInputIdMap, inputId);
274            if (hardwareIndex >= 0) {
275                mHardwareInputIdMap.removeAt(hardwareIndex);
276            }
277            int cecIndex = indexOfEqualValue(mHdmiCecInputIdMap, inputId);
278            if (cecIndex >= 0) {
279                mHdmiCecInputIdMap.removeAt(cecIndex);
280            }
281        }
282    }
283
284    /**
285     * Create a TvInputHardware object with a specific deviceId. One service at a time can access
286     * the object, and if more than one process attempts to create hardware with the same deviceId,
287     * the latest service will get the object and all the other hardware are released. The
288     * release is notified via ITvInputHardwareCallback.onReleased().
289     */
290    public ITvInputHardware acquireHardware(int deviceId, ITvInputHardwareCallback callback,
291            TvInputInfo info, int callingUid, int resolvedUserId) {
292        if (callback == null) {
293            throw new NullPointerException();
294        }
295        synchronized (mLock) {
296            Connection connection = mConnections.get(deviceId);
297            if (connection == null) {
298                Slog.e(TAG, "Invalid deviceId : " + deviceId);
299                return null;
300            }
301            if (checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
302                TvInputHardwareImpl hardware =
303                        new TvInputHardwareImpl(connection.getHardwareInfoLocked());
304                try {
305                    callback.asBinder().linkToDeath(connection, 0);
306                } catch (RemoteException e) {
307                    hardware.release();
308                    return null;
309                }
310                connection.resetLocked(hardware, callback, info, callingUid, resolvedUserId);
311            }
312            return connection.getHardwareLocked();
313        }
314    }
315
316    /**
317     * Release the specified hardware.
318     */
319    public void releaseHardware(int deviceId, ITvInputHardware hardware, int callingUid,
320            int resolvedUserId) {
321        synchronized (mLock) {
322            Connection connection = mConnections.get(deviceId);
323            if (connection == null) {
324                Slog.e(TAG, "Invalid deviceId : " + deviceId);
325                return;
326            }
327            if (connection.getHardwareLocked() != hardware
328                    || checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
329                return;
330            }
331            connection.resetLocked(null, null, null, null, null);
332        }
333    }
334
335    private String findInputIdForHdmiPortLocked(int port) {
336        for (TvInputHardwareInfo hardwareInfo : mInfoList) {
337            if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI
338                    && hardwareInfo.getHdmiPortId() == port) {
339                return mHardwareInputIdMap.get(hardwareInfo.getDeviceId());
340            }
341        }
342        return null;
343    }
344
345    private class Connection implements IBinder.DeathRecipient {
346        private final TvInputHardwareInfo mHardwareInfo;
347        private TvInputInfo mInfo;
348        private TvInputHardwareImpl mHardware = null;
349        private ITvInputHardwareCallback mCallback;
350        private TvStreamConfig[] mConfigs = null;
351        private Integer mCallingUid = null;
352        private Integer mResolvedUserId = null;
353
354        public Connection(TvInputHardwareInfo hardwareInfo) {
355            mHardwareInfo = hardwareInfo;
356        }
357
358        // *Locked methods assume TvInputHardwareManager.mLock is held.
359
360        public void resetLocked(TvInputHardwareImpl hardware, ITvInputHardwareCallback callback,
361                TvInputInfo info, Integer callingUid, Integer resolvedUserId) {
362            if (mHardware != null) {
363                try {
364                    mCallback.onReleased();
365                } catch (RemoteException e) {
366                    Slog.e(TAG, "error in Connection::resetLocked", e);
367                }
368                mHardware.release();
369            }
370            mHardware = hardware;
371            mCallback = callback;
372            mInfo = info;
373            mCallingUid = callingUid;
374            mResolvedUserId = resolvedUserId;
375
376            if (mHardware != null && mCallback != null) {
377                try {
378                    mCallback.onStreamConfigChanged(getConfigsLocked());
379                } catch (RemoteException e) {
380                    Slog.e(TAG, "error in Connection::resetLocked", e);
381                }
382            }
383        }
384
385        public void updateConfigsLocked(TvStreamConfig[] configs) {
386            mConfigs = configs;
387        }
388
389        public TvInputHardwareInfo getHardwareInfoLocked() {
390            return mHardwareInfo;
391        }
392
393        public TvInputInfo getInfoLocked() {
394            return mInfo;
395        }
396
397        public ITvInputHardware getHardwareLocked() {
398            return mHardware;
399        }
400
401        public ITvInputHardwareCallback getCallbackLocked() {
402            return mCallback;
403        }
404
405        public TvStreamConfig[] getConfigsLocked() {
406            return mConfigs;
407        }
408
409        public Integer getCallingUidLocked() {
410            return mCallingUid;
411        }
412
413        public Integer getResolvedUserIdLocked() {
414            return mResolvedUserId;
415        }
416
417        @Override
418        public void binderDied() {
419            synchronized (mLock) {
420                resetLocked(null, null, null, null, null);
421            }
422        }
423    }
424
425    private class TvInputHardwareImpl extends ITvInputHardware.Stub {
426        private final TvInputHardwareInfo mInfo;
427        private boolean mReleased = false;
428        private final Object mImplLock = new Object();
429
430        private final AudioDevicePort mAudioSource;
431        private final AudioDevicePort mAudioSink;
432        private AudioPatch mAudioPatch = null;
433
434        private TvStreamConfig mActiveConfig = null;
435
436        public TvInputHardwareImpl(TvInputHardwareInfo info) {
437            mInfo = info;
438            AudioDevicePort audioSource = null;
439            AudioDevicePort audioSink = null;
440            if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) {
441                ArrayList<AudioPort> devicePorts = new ArrayList<AudioPort>();
442                if (mAudioManager.listAudioDevicePorts(devicePorts) == AudioManager.SUCCESS) {
443                    // Find source
444                    for (AudioPort port : devicePorts) {
445                        AudioDevicePort devicePort = (AudioDevicePort) port;
446                        if (devicePort.type() == mInfo.getAudioType() &&
447                                devicePort.address().equals(mInfo.getAudioAddress())) {
448                            audioSource = devicePort;
449                            break;
450                        }
451                    }
452                    // Find sink
453                    // TODO: App may want to specify sink device?
454                    int sinkDevices = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
455                    for (AudioPort port : devicePorts) {
456                        AudioDevicePort devicePort = (AudioDevicePort) port;
457                        if (devicePort.type() == sinkDevices) {
458                            audioSink = devicePort;
459                            break;
460                        }
461                    }
462                }
463            }
464            mAudioSource = audioSource;
465            mAudioSink = audioSink;
466        }
467
468        public void release() {
469            synchronized (mImplLock) {
470                if (mAudioPatch != null) {
471                    mAudioManager.releaseAudioPatch(mAudioPatch);
472                    mAudioPatch = null;
473                }
474                mReleased = true;
475            }
476        }
477
478        // A TvInputHardwareImpl object holds only one active session. Therefore, if a client
479        // attempts to call setSurface with different TvStreamConfig objects, the last call will
480        // prevail.
481        @Override
482        public boolean setSurface(Surface surface, TvStreamConfig config)
483                throws RemoteException {
484            synchronized (mImplLock) {
485                if (mReleased) {
486                    throw new IllegalStateException("Device already released.");
487                }
488                if (surface != null && config == null) {
489                    return false;
490                }
491                if (surface == null && mActiveConfig == null) {
492                    return false;
493                }
494                if (mInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
495                    if (surface != null) {
496                        // Set "Active Source" for HDMI.
497                        // TODO(hdmi): mHdmiClient.deviceSelect(...);
498                        mActiveHdmiSources.add(mInfo.getDeviceId());
499                    } else {
500                        mActiveHdmiSources.remove(mInfo.getDeviceId());
501                        if (mActiveHdmiSources.size() == 0) {
502                            // Tell HDMI that no HDMI source is active
503                            // TODO(hdmi): mHdmiClient.portSelect(null);
504                        }
505                    }
506                }
507                if (mAudioSource != null && mAudioSink != null) {
508                    if (surface != null) {
509                        AudioPortConfig sourceConfig = mAudioSource.activeConfig();
510                        AudioPortConfig sinkConfig = mAudioSink.activeConfig();
511                        AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch };
512                        // TODO: build config if activeConfig() == null
513                        mAudioManager.createAudioPatch(
514                                audioPatchArray,
515                                new AudioPortConfig[] { sourceConfig },
516                                new AudioPortConfig[] { sinkConfig });
517                        mAudioPatch = audioPatchArray[0];
518                    } else {
519                        mAudioManager.releaseAudioPatch(mAudioPatch);
520                        mAudioPatch = null;
521                    }
522                }
523                int result = TvInputHal.ERROR_UNKNOWN;
524                if (surface == null) {
525                    result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
526                    mActiveConfig = null;
527                } else {
528                    if (config != mActiveConfig && mActiveConfig != null) {
529                        result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
530                        if (result != TvInputHal.SUCCESS) {
531                            mActiveConfig = null;
532                            return false;
533                        }
534                    }
535                    result = mHal.addStream(mInfo.getDeviceId(), surface, config);
536                    if (result == TvInputHal.SUCCESS) {
537                        mActiveConfig = config;
538                    }
539                }
540                return result == TvInputHal.SUCCESS;
541            }
542        }
543
544        @Override
545        public void setVolume(float volume) throws RemoteException {
546            synchronized (mImplLock) {
547                if (mReleased) {
548                    throw new IllegalStateException("Device already released.");
549                }
550            }
551            // TODO: Use AudioGain?
552        }
553
554        @Override
555        public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException {
556            synchronized (mImplLock) {
557                if (mReleased) {
558                    throw new IllegalStateException("Device already released.");
559                }
560            }
561            if (mInfo.getType() != TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
562                return false;
563            }
564            // TODO(hdmi): mHdmiClient.sendKeyEvent(event);
565            return false;
566        }
567    }
568
569    interface Listener {
570        public void onStateChanged(String inputId, int state);
571        public void onHardwareDeviceAdded(TvInputHardwareInfo info);
572        public void onHardwareDeviceRemoved(TvInputHardwareInfo info);
573        public void onHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDevice);
574        public void onHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDevice);
575    }
576
577    private class ListenerHandler extends Handler {
578        private static final int STATE_CHANGED = 1;
579        private static final int HARDWARE_DEVICE_ADDED = 2;
580        private static final int HARDWARE_DEVICE_REMOVED = 3;
581        private static final int HDMI_CEC_DEVICE_ADDED = 4;
582        private static final int HDMI_CEC_DEVICE_REMOVED = 5;
583
584        @Override
585        public final void handleMessage(Message msg) {
586            switch (msg.what) {
587                case STATE_CHANGED: {
588                    String inputId = (String) msg.obj;
589                    int state = msg.arg1;
590                    mListener.onStateChanged(inputId, state);
591                    break;
592                }
593                case HARDWARE_DEVICE_ADDED: {
594                    TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj;
595                    mListener.onHardwareDeviceAdded(info);
596                    break;
597                }
598                case HARDWARE_DEVICE_REMOVED: {
599                    TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj;
600                    mListener.onHardwareDeviceRemoved(info);
601                    break;
602                }
603                case HDMI_CEC_DEVICE_ADDED: {
604                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
605                    mListener.onHdmiCecDeviceAdded(info);
606                    break;
607                }
608                case HDMI_CEC_DEVICE_REMOVED: {
609                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
610                    mListener.onHdmiCecDeviceRemoved(info);
611                    break;
612                }
613                default: {
614                    Slog.w(TAG, "Unhandled message: " + msg);
615                    break;
616                }
617            }
618        }
619    }
620
621    // Listener implementations for HdmiControlService
622
623    private final class HdmiHotplugEventListener extends IHdmiHotplugEventListener.Stub {
624        @Override
625        public void onReceived(HdmiHotplugEvent event) {
626            synchronized (mLock) {
627                mHdmiStateMap.put(event.getPort(), event.isConnected());
628                String inputId = findInputIdForHdmiPortLocked(event.getPort());
629                if (inputId == null) {
630                    return;
631                }
632                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
633                        convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget();
634            }
635        }
636    }
637
638    private final class HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub {
639        @Override
640        public void onStatusChanged(HdmiCecDeviceInfo deviceInfo, boolean activated) {
641            mHandler.obtainMessage(
642                    activated ? ListenerHandler.HDMI_CEC_DEVICE_ADDED
643                    : ListenerHandler.HDMI_CEC_DEVICE_REMOVED,
644                    0, 0, deviceInfo).sendToTarget();
645        }
646    }
647
648    private final class HdmiInputChangeListener extends IHdmiInputChangeListener.Stub {
649        @Override
650        public void onChanged(HdmiCecDeviceInfo device) throws RemoteException {
651            String inputId = mHdmiCecInputMap.get(device.getLogicalAddress());
652            if (inputId != null) {
653                Intent intent = new Intent(Intent.ACTION_VIEW);
654                intent.setData(TvContract.buildChannelUriForPassthroughTvInput(inputId));
655                mContext.startActivity(intent);
656            }
657        }
658    }
659}
660