TvInputHardwareManager.java revision 4f9f57cede3de2e2aa3045e04b485b176ab22dbd
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    public void addHdmiCecTvInput(int logicalAddress, TvInputInfo info) {
239        if (info.getType() != TvInputInfo.TYPE_HDMI) {
240            throw new IllegalArgumentException("info (" + info + ") has non-HDMI type.");
241        }
242        synchronized (mLock) {
243            String parentId = info.getParentId();
244            int parentIndex = mHardwareInputIdMap.indexOfValue(parentId);
245            if (parentIndex < 0 || !parentId.equals(mHardwareInputIdMap.valueAt(parentIndex))) {
246                throw new IllegalArgumentException("info (" + info + ") has invalid parentId.");
247            }
248            String oldInputId = mHdmiCecInputIdMap.get(logicalAddress);
249            if (oldInputId != null) {
250                Slog.w(TAG, "Trying to override previous registration: old = "
251                        + mInputMap.get(oldInputId) + ":" + logicalAddress + ", new = "
252                        + info + ":" + logicalAddress);
253            }
254            mHdmiCecInputIdMap.put(logicalAddress, info.getId());
255            mInputMap.put(info.getId(), info);
256        }
257    }
258
259    public void removeTvInput(String inputId) {
260        synchronized (mLock) {
261            mInputMap.remove(inputId);
262            int hardwareIndex = mHardwareInputIdMap.indexOfValue(inputId);
263            if (hardwareIndex >= 0) {
264                mHardwareInputIdMap.removeAt(hardwareIndex);
265            }
266            int cecIndex = mHdmiCecInputIdMap.indexOfValue(inputId);
267            if (cecIndex >= 0) {
268                mHdmiCecInputIdMap.removeAt(cecIndex);
269            }
270        }
271    }
272
273    /**
274     * Create a TvInputHardware object with a specific deviceId. One service at a time can access
275     * the object, and if more than one process attempts to create hardware with the same deviceId,
276     * the latest service will get the object and all the other hardware are released. The
277     * release is notified via ITvInputHardwareCallback.onReleased().
278     */
279    public ITvInputHardware acquireHardware(int deviceId, ITvInputHardwareCallback callback,
280            TvInputInfo info, int callingUid, int resolvedUserId) {
281        if (callback == null) {
282            throw new NullPointerException();
283        }
284        synchronized (mLock) {
285            Connection connection = mConnections.get(deviceId);
286            if (connection == null) {
287                Slog.e(TAG, "Invalid deviceId : " + deviceId);
288                return null;
289            }
290            if (checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
291                TvInputHardwareImpl hardware =
292                        new TvInputHardwareImpl(connection.getHardwareInfoLocked());
293                try {
294                    callback.asBinder().linkToDeath(connection, 0);
295                } catch (RemoteException e) {
296                    hardware.release();
297                    return null;
298                }
299                connection.resetLocked(hardware, callback, info, callingUid, resolvedUserId);
300            }
301            return connection.getHardwareLocked();
302        }
303    }
304
305    /**
306     * Release the specified hardware.
307     */
308    public void releaseHardware(int deviceId, ITvInputHardware hardware, int callingUid,
309            int resolvedUserId) {
310        synchronized (mLock) {
311            Connection connection = mConnections.get(deviceId);
312            if (connection == null) {
313                Slog.e(TAG, "Invalid deviceId : " + deviceId);
314                return;
315            }
316            if (connection.getHardwareLocked() != hardware
317                    || checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
318                return;
319            }
320            connection.resetLocked(null, null, null, null, null);
321        }
322    }
323
324    private String findInputIdForHdmiPortLocked(int port) {
325        for (TvInputHardwareInfo hardwareInfo : mInfoList) {
326            if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI
327                    && hardwareInfo.getHdmiPortId() == port) {
328                return mHardwareInputIdMap.get(hardwareInfo.getDeviceId());
329            }
330        }
331        return null;
332    }
333
334    private class Connection implements IBinder.DeathRecipient {
335        private final TvInputHardwareInfo mHardwareInfo;
336        private TvInputInfo mInfo;
337        private TvInputHardwareImpl mHardware = null;
338        private ITvInputHardwareCallback mCallback;
339        private TvStreamConfig[] mConfigs = null;
340        private Integer mCallingUid = null;
341        private Integer mResolvedUserId = null;
342
343        public Connection(TvInputHardwareInfo hardwareInfo) {
344            mHardwareInfo = hardwareInfo;
345        }
346
347        // *Locked methods assume TvInputHardwareManager.mLock is held.
348
349        public void resetLocked(TvInputHardwareImpl hardware, ITvInputHardwareCallback callback,
350                TvInputInfo info, Integer callingUid, Integer resolvedUserId) {
351            if (mHardware != null) {
352                try {
353                    mCallback.onReleased();
354                } catch (RemoteException e) {
355                    Slog.e(TAG, "error in Connection::resetLocked", e);
356                }
357                mHardware.release();
358            }
359            mHardware = hardware;
360            mCallback = callback;
361            mInfo = info;
362            mCallingUid = callingUid;
363            mResolvedUserId = resolvedUserId;
364
365            if (mHardware != null && mCallback != null) {
366                try {
367                    mCallback.onStreamConfigChanged(getConfigsLocked());
368                } catch (RemoteException e) {
369                    Slog.e(TAG, "error in Connection::resetLocked", e);
370                }
371            }
372        }
373
374        public void updateConfigsLocked(TvStreamConfig[] configs) {
375            mConfigs = configs;
376        }
377
378        public TvInputHardwareInfo getHardwareInfoLocked() {
379            return mHardwareInfo;
380        }
381
382        public TvInputInfo getInfoLocked() {
383            return mInfo;
384        }
385
386        public ITvInputHardware getHardwareLocked() {
387            return mHardware;
388        }
389
390        public ITvInputHardwareCallback getCallbackLocked() {
391            return mCallback;
392        }
393
394        public TvStreamConfig[] getConfigsLocked() {
395            return mConfigs;
396        }
397
398        public Integer getCallingUidLocked() {
399            return mCallingUid;
400        }
401
402        public Integer getResolvedUserIdLocked() {
403            return mResolvedUserId;
404        }
405
406        @Override
407        public void binderDied() {
408            synchronized (mLock) {
409                resetLocked(null, null, null, null, null);
410            }
411        }
412    }
413
414    private class TvInputHardwareImpl extends ITvInputHardware.Stub {
415        private final TvInputHardwareInfo mInfo;
416        private boolean mReleased = false;
417        private final Object mImplLock = new Object();
418
419        private final AudioDevicePort mAudioSource;
420        private final AudioDevicePort mAudioSink;
421        private AudioPatch mAudioPatch = null;
422
423        private TvStreamConfig mActiveConfig = null;
424
425        public TvInputHardwareImpl(TvInputHardwareInfo info) {
426            mInfo = info;
427            AudioDevicePort audioSource = null;
428            AudioDevicePort audioSink = null;
429            if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) {
430                ArrayList<AudioPort> devicePorts = new ArrayList<AudioPort>();
431                if (mAudioManager.listAudioDevicePorts(devicePorts) == AudioManager.SUCCESS) {
432                    // Find source
433                    for (AudioPort port : devicePorts) {
434                        AudioDevicePort devicePort = (AudioDevicePort) port;
435                        if (devicePort.type() == mInfo.getAudioType() &&
436                                devicePort.address().equals(mInfo.getAudioAddress())) {
437                            audioSource = devicePort;
438                            break;
439                        }
440                    }
441                    // Find sink
442                    // TODO: App may want to specify sink device?
443                    int sinkDevices = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
444                    for (AudioPort port : devicePorts) {
445                        AudioDevicePort devicePort = (AudioDevicePort) port;
446                        if (devicePort.type() == sinkDevices) {
447                            audioSink = devicePort;
448                            break;
449                        }
450                    }
451                }
452            }
453            mAudioSource = audioSource;
454            mAudioSink = audioSink;
455        }
456
457        public void release() {
458            synchronized (mImplLock) {
459                if (mAudioPatch != null) {
460                    mAudioManager.releaseAudioPatch(mAudioPatch);
461                    mAudioPatch = null;
462                }
463                mReleased = true;
464            }
465        }
466
467        // A TvInputHardwareImpl object holds only one active session. Therefore, if a client
468        // attempts to call setSurface with different TvStreamConfig objects, the last call will
469        // prevail.
470        @Override
471        public boolean setSurface(Surface surface, TvStreamConfig config)
472                throws RemoteException {
473            synchronized (mImplLock) {
474                if (mReleased) {
475                    throw new IllegalStateException("Device already released.");
476                }
477                if (surface != null && config == null) {
478                    return false;
479                }
480                if (surface == null && mActiveConfig == null) {
481                    return false;
482                }
483                if (mInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
484                    if (surface != null) {
485                        // Set "Active Source" for HDMI.
486                        // TODO(hdmi): mHdmiClient.deviceSelect(...);
487                        mActiveHdmiSources.add(mInfo.getDeviceId());
488                    } else {
489                        mActiveHdmiSources.remove(mInfo.getDeviceId());
490                        if (mActiveHdmiSources.size() == 0) {
491                            // Tell HDMI that no HDMI source is active
492                            // TODO(hdmi): mHdmiClient.portSelect(null);
493                        }
494                    }
495                }
496                if (mAudioSource != null && mAudioSink != null) {
497                    if (surface != null) {
498                        AudioPortConfig sourceConfig = mAudioSource.activeConfig();
499                        AudioPortConfig sinkConfig = mAudioSink.activeConfig();
500                        AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch };
501                        // TODO: build config if activeConfig() == null
502                        mAudioManager.createAudioPatch(
503                                audioPatchArray,
504                                new AudioPortConfig[] { sourceConfig },
505                                new AudioPortConfig[] { sinkConfig });
506                        mAudioPatch = audioPatchArray[0];
507                    } else {
508                        mAudioManager.releaseAudioPatch(mAudioPatch);
509                        mAudioPatch = null;
510                    }
511                }
512                int result = TvInputHal.ERROR_UNKNOWN;
513                if (surface == null) {
514                    result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
515                    mActiveConfig = null;
516                } else {
517                    if (config != mActiveConfig && mActiveConfig != null) {
518                        result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
519                        if (result != TvInputHal.SUCCESS) {
520                            mActiveConfig = null;
521                            return false;
522                        }
523                    }
524                    result = mHal.addStream(mInfo.getDeviceId(), surface, config);
525                    if (result == TvInputHal.SUCCESS) {
526                        mActiveConfig = config;
527                    }
528                }
529                return result == TvInputHal.SUCCESS;
530            }
531        }
532
533        @Override
534        public void setVolume(float volume) throws RemoteException {
535            synchronized (mImplLock) {
536                if (mReleased) {
537                    throw new IllegalStateException("Device already released.");
538                }
539            }
540            // TODO: Use AudioGain?
541        }
542
543        @Override
544        public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException {
545            synchronized (mImplLock) {
546                if (mReleased) {
547                    throw new IllegalStateException("Device already released.");
548                }
549            }
550            if (mInfo.getType() != TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
551                return false;
552            }
553            // TODO(hdmi): mHdmiClient.sendKeyEvent(event);
554            return false;
555        }
556    }
557
558    interface Listener {
559        public void onStateChanged(String inputId, int state);
560        public void onHardwareDeviceAdded(TvInputHardwareInfo info);
561        public void onHardwareDeviceRemoved(TvInputHardwareInfo info);
562        public void onHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDevice);
563        public void onHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDevice);
564    }
565
566    private class ListenerHandler extends Handler {
567        private static final int STATE_CHANGED = 1;
568        private static final int HARDWARE_DEVICE_ADDED = 2;
569        private static final int HARDWARE_DEVICE_REMOVED = 3;
570        private static final int HDMI_CEC_DEVICE_ADDED = 4;
571        private static final int HDMI_CEC_DEVICE_REMOVED = 5;
572
573        @Override
574        public final void handleMessage(Message msg) {
575            switch (msg.what) {
576                case STATE_CHANGED: {
577                    String inputId = (String) msg.obj;
578                    int state = msg.arg1;
579                    mListener.onStateChanged(inputId, state);
580                    break;
581                }
582                case HARDWARE_DEVICE_ADDED: {
583                    TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj;
584                    mListener.onHardwareDeviceAdded(info);
585                    break;
586                }
587                case HARDWARE_DEVICE_REMOVED: {
588                    TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj;
589                    mListener.onHardwareDeviceRemoved(info);
590                    break;
591                }
592                case HDMI_CEC_DEVICE_ADDED: {
593                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
594                    mListener.onHdmiCecDeviceAdded(info);
595                    break;
596                }
597                case HDMI_CEC_DEVICE_REMOVED: {
598                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
599                    mListener.onHdmiCecDeviceRemoved(info);
600                    break;
601                }
602                default: {
603                    Slog.w(TAG, "Unhandled message: " + msg);
604                    break;
605                }
606            }
607        }
608    }
609
610    // Listener implementations for HdmiControlService
611
612    private final class HdmiHotplugEventListener extends IHdmiHotplugEventListener.Stub {
613        @Override
614        public void onReceived(HdmiHotplugEvent event) {
615            synchronized (mLock) {
616                mHdmiStateMap.put(event.getPort(), event.isConnected());
617                String inputId = findInputIdForHdmiPortLocked(event.getPort());
618                if (inputId == null) {
619                    return;
620                }
621                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
622                        convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget();
623            }
624        }
625    }
626
627    private final class HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub {
628        @Override
629        public void onStatusChanged(HdmiCecDeviceInfo deviceInfo, boolean activated) {
630            mHandler.obtainMessage(
631                    activated ? ListenerHandler.HDMI_CEC_DEVICE_ADDED
632                    : ListenerHandler.HDMI_CEC_DEVICE_REMOVED,
633                    0, 0, deviceInfo).sendToTarget();
634        }
635    }
636
637    private final class HdmiInputChangeListener extends IHdmiInputChangeListener.Stub {
638        @Override
639        public void onChanged(HdmiCecDeviceInfo device) throws RemoteException {
640            // TODO: Build a channel Uri for the TvInputInfo associated with the logical device
641            //       and send an intent to TV app
642        }
643    }
644}
645