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