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