HdmiControlService.java revision a062a9339add79a84862a34e363e3e454a6ec435
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.hdmi; 18 19import android.annotation.Nullable; 20import android.content.Context; 21import android.hardware.hdmi.HdmiCec; 22import android.hardware.hdmi.HdmiCecDeviceInfo; 23import android.hardware.hdmi.HdmiCecMessage; 24import android.hardware.hdmi.HdmiHotplugEvent; 25import android.hardware.hdmi.HdmiPortInfo; 26import android.hardware.hdmi.IHdmiControlCallback; 27import android.hardware.hdmi.IHdmiControlService; 28import android.hardware.hdmi.IHdmiDeviceEventListener; 29import android.hardware.hdmi.IHdmiHotplugEventListener; 30import android.os.Build; 31import android.os.Handler; 32import android.os.HandlerThread; 33import android.os.IBinder; 34import android.os.Looper; 35import android.os.RemoteException; 36import android.util.Slog; 37import android.util.SparseArray; 38import android.util.SparseIntArray; 39 40import com.android.server.SystemService; 41import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback; 42 43import java.util.ArrayList; 44import java.util.Collections; 45import java.util.List; 46 47/** 48 * Provides a service for sending and processing HDMI control messages, 49 * HDMI-CEC and MHL control command, and providing the information on both standard. 50 */ 51public final class HdmiControlService extends SystemService { 52 private static final String TAG = "HdmiControlService"; 53 54 // TODO: Rename the permission to HDMI_CONTROL. 55 private static final String PERMISSION = "android.permission.HDMI_CEC"; 56 57 /** 58 * Interface to report send result. 59 */ 60 interface SendMessageCallback { 61 /** 62 * Called when {@link HdmiControlService#sendCecCommand} is completed. 63 * 64 * @param error result of send request. 65 * @see {@link #SEND_RESULT_SUCCESS} 66 * @see {@link #SEND_RESULT_NAK} 67 * @see {@link #SEND_RESULT_FAILURE} 68 */ 69 void onSendCompleted(int error); 70 } 71 72 /** 73 * Interface to get a list of available logical devices. 74 */ 75 interface DevicePollingCallback { 76 /** 77 * Called when device polling is finished. 78 * 79 * @param ackedAddress a list of logical addresses of available devices 80 */ 81 void onPollingFinished(List<Integer> ackedAddress); 82 } 83 84 // A thread to handle synchronous IO of CEC and MHL control service. 85 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 86 // and sparse call it shares a thread to handle IO operations. 87 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 88 89 // Used to synchronize the access to the service. 90 private final Object mLock = new Object(); 91 92 // Type of logical devices hosted in the system. Stored in the unmodifiable list. 93 private final List<Integer> mLocalDevices; 94 95 // List of listeners registered by callers that want to get notified of 96 // hotplug events. 97 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 98 99 // List of records for hotplug event listener to handle the the caller killed in action. 100 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 101 new ArrayList<>(); 102 103 // List of listeners registered by callers that want to get notified of 104 // device status events. 105 private final ArrayList<IHdmiDeviceEventListener> mDeviceEventListeners = new ArrayList<>(); 106 107 // List of records for device event listener to handle the the caller killed in action. 108 private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords = 109 new ArrayList<>(); 110 111 // Handler running on service thread. It's used to run a task in service thread. 112 private final Handler mHandler = new Handler(); 113 114 @Nullable 115 private HdmiCecController mCecController; 116 117 @Nullable 118 private HdmiMhlController mMhlController; 119 120 // HDMI port information. Stored in the unmodifiable list to keep the static information 121 // from being modified. 122 private List<HdmiPortInfo> mPortInfo; 123 124 public HdmiControlService(Context context) { 125 super(context); 126 mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray( 127 com.android.internal.R.array.config_hdmiCecLogicalDeviceType)); 128 } 129 130 @Override 131 public void onStart() { 132 mIoThread.start(); 133 mCecController = HdmiCecController.create(this); 134 135 if (mCecController != null) { 136 initializeLocalDevices(mLocalDevices); 137 } else { 138 Slog.i(TAG, "Device does not support HDMI-CEC."); 139 } 140 141 mMhlController = HdmiMhlController.create(this); 142 if (mMhlController == null) { 143 Slog.i(TAG, "Device does not support MHL-control."); 144 } 145 mPortInfo = initPortInfo(); 146 publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); 147 148 // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and 149 // start to monitor the preference value and invoke SystemAudioActionFromTv if needed. 150 } 151 152 private void initializeLocalDevices(final List<Integer> deviceTypes) { 153 // A container for [Logical Address, Local device info]. 154 final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>(); 155 final SparseIntArray finished = new SparseIntArray(); 156 for (int type : deviceTypes) { 157 final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type); 158 localDevice.init(); 159 mCecController.allocateLogicalAddress(type, 160 localDevice.getPreferredAddress(), new AllocateAddressCallback() { 161 @Override 162 public void onAllocated(int deviceType, int logicalAddress) { 163 if (logicalAddress == HdmiCec.ADDR_UNREGISTERED) { 164 Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]"); 165 } else { 166 HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType); 167 localDevice.setDeviceInfo(deviceInfo); 168 mCecController.addLocalDevice(deviceType, localDevice); 169 mCecController.addLogicalAddress(logicalAddress); 170 devices.append(logicalAddress, localDevice); 171 } 172 finished.append(deviceType, logicalAddress); 173 174 // Once finish address allocation for all devices, notify 175 // it to each device. 176 if (deviceTypes.size() == finished.size()) { 177 notifyAddressAllocated(devices); 178 } 179 } 180 }); 181 } 182 } 183 184 private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices) { 185 for (int i = 0; i < devices.size(); ++i) { 186 int address = devices.keyAt(i); 187 HdmiCecLocalDevice device = devices.valueAt(i); 188 device.onAddressAllocated(address); 189 } 190 } 191 192 // Initialize HDMI port information. Combine the information from CEC and MHL HAL and 193 // keep them in one place. 194 private List<HdmiPortInfo> initPortInfo() { 195 HdmiPortInfo[] cecPortInfo = null; 196 197 // CEC HAL provides majority of the info while MHL does only MHL support flag for 198 // each port. Return empty array if CEC HAL didn't provide the info. 199 if (mCecController != null) { 200 cecPortInfo = mCecController.getPortInfos(); 201 } 202 if (cecPortInfo == null) { 203 return Collections.emptyList(); 204 } 205 206 HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0]; 207 if (mMhlController != null) { 208 // TODO: Implement plumbing logic to get MHL port information. 209 // mhlPortInfo = mMhlController.getPortInfos(); 210 } 211 212 // Use the id (port number) to find the matched info between CEC and MHL to combine them 213 // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found. 214 ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length); 215 for (int i = 0; i < cecPortInfo.length; ++i) { 216 HdmiPortInfo cec = cecPortInfo[i]; 217 int id = cec.getId(); 218 boolean mhlInfoFound = false; 219 for (HdmiPortInfo mhl : mhlPortInfo) { 220 if (id == mhl.getId()) { 221 result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(), 222 cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported())); 223 mhlInfoFound = true; 224 break; 225 } 226 } 227 if (!mhlInfoFound) { 228 result.add(cec); 229 } 230 } 231 232 return Collections.unmodifiableList(result); 233 } 234 235 /** 236 * Returns HDMI port information for the given port id. 237 * 238 * @param portId HDMI port id 239 * @return {@link HdmiPortInfo} for the given port 240 */ 241 HdmiPortInfo getPortInfo(int portId) { 242 // mPortInfo is an unmodifiable list and the only reference to its inner list. 243 // No lock is necessary. 244 for (HdmiPortInfo info : mPortInfo) { 245 if (portId == info.getId()) { 246 return info; 247 } 248 } 249 return null; 250 } 251 252 /** 253 * Returns the routing path (physical address) of the HDMI port for the given 254 * port id. 255 */ 256 int portIdToPath(int portId) { 257 HdmiPortInfo portInfo = getPortInfo(portId); 258 if (portInfo == null) { 259 Slog.e(TAG, "Cannot find the port info: " + portId); 260 return HdmiConstants.INVALID_PHYSICAL_ADDRESS; 261 } 262 return portInfo.getAddress(); 263 } 264 265 /** 266 * Returns the id of HDMI port located at the top of the hierarchy of 267 * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance, 268 * the port id to be returned is the ID associated with the port address 269 * 0x1000 (1.0.0.0) which is the topmost path of the given routing path. 270 */ 271 int pathToPortId(int path) { 272 int portAddress = path & HdmiConstants.ROUTING_PATH_TOP_MASK; 273 for (HdmiPortInfo info : mPortInfo) { 274 if (portAddress == info.getAddress()) { 275 return info.getId(); 276 } 277 } 278 return HdmiConstants.INVALID_PORT_ID; 279 } 280 281 /** 282 * Returns {@link Looper} for IO operation. 283 * 284 * <p>Declared as package-private. 285 */ 286 Looper getIoLooper() { 287 return mIoThread.getLooper(); 288 } 289 290 /** 291 * Returns {@link Looper} of main thread. Use this {@link Looper} instance 292 * for tasks that are running on main service thread. 293 * 294 * <p>Declared as package-private. 295 */ 296 Looper getServiceLooper() { 297 return mHandler.getLooper(); 298 } 299 300 /** 301 * Returns physical address of the device. 302 */ 303 int getPhysicalAddress() { 304 return mCecController.getPhysicalAddress(); 305 } 306 307 /** 308 * Returns vendor id of CEC service. 309 */ 310 int getVendorId() { 311 return mCecController.getVendorId(); 312 } 313 314 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 315 assertRunOnServiceThread(); 316 HdmiCecLocalDeviceTv tv = tv(); 317 if (tv == null) { 318 return null; 319 } 320 return tv.getDeviceInfo(logicalAddress); 321 } 322 323 /** 324 * Returns version of CEC. 325 */ 326 int getCecVersion() { 327 return mCecController.getVersion(); 328 } 329 330 /** 331 * Whether a device of the specified physical address is connected to ARC enabled port. 332 */ 333 boolean isConnectedToArcPort(int physicalAddress) { 334 for (HdmiPortInfo portInfo : mPortInfo) { 335 if (hasSameTopPort(portInfo.getAddress(), physicalAddress) 336 && portInfo.isArcSupported()) { 337 return true; 338 } 339 } 340 return false; 341 } 342 343 void runOnServiceThread(Runnable runnable) { 344 mHandler.post(runnable); 345 } 346 347 void runOnServiceThreadAtFrontOfQueue(Runnable runnable) { 348 mHandler.postAtFrontOfQueue(runnable); 349 } 350 351 private void assertRunOnServiceThread() { 352 if (Looper.myLooper() != mHandler.getLooper()) { 353 throw new IllegalStateException("Should run on service thread."); 354 } 355 } 356 357 /** 358 * Transmit a CEC command to CEC bus. 359 * 360 * @param command CEC command to send out 361 * @param callback interface used to the result of send command 362 */ 363 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { 364 mCecController.sendCommand(command, callback); 365 } 366 367 void sendCecCommand(HdmiCecMessage command) { 368 mCecController.sendCommand(command, null); 369 } 370 371 boolean handleCecCommand(HdmiCecMessage message) { 372 return dispatchMessageToLocalDevice(message); 373 } 374 375 void setAudioReturnChannel(boolean enabled) { 376 mCecController.setAudioReturnChannel(enabled); 377 } 378 379 private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) { 380 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 381 if (device.dispatchMessage(message) 382 && message.getDestination() != HdmiCec.ADDR_BROADCAST) { 383 return true; 384 } 385 } 386 387 Slog.w(TAG, "Unhandled cec command:" + message); 388 return false; 389 } 390 391 /** 392 * Called when a new hotplug event is issued. 393 * 394 * @param portNo hdmi port number where hot plug event issued. 395 * @param connected whether to be plugged in or not 396 */ 397 void onHotplug(int portNo, boolean connected) { 398 assertRunOnServiceThread(); 399 400 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 401 device.onHotplug(portNo, connected); 402 } 403 404 announceHotplugEvent(portNo, connected); 405 } 406 407 /** 408 * Poll all remote devices. It sends <Polling Message> to all remote 409 * devices. 410 * 411 * @param callback an interface used to get a list of all remote devices' address 412 * @param pickStrategy strategy how to pick polling candidates 413 * @param retryCount the number of retry used to send polling message to remote devices 414 * @throw IllegalArgumentException if {@code pickStrategy} is invalid value 415 */ 416 void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) { 417 mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount); 418 } 419 420 private int checkPollStrategy(int pickStrategy) { 421 int strategy = pickStrategy & HdmiConstants.POLL_STRATEGY_MASK; 422 if (strategy == 0) { 423 throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy); 424 } 425 int iterationStrategy = pickStrategy & HdmiConstants.POLL_ITERATION_STRATEGY_MASK; 426 if (iterationStrategy == 0) { 427 throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy); 428 } 429 return strategy | iterationStrategy; 430 } 431 432 List<HdmiCecLocalDevice> getAllLocalDevices() { 433 assertRunOnServiceThread(); 434 return mCecController.getLocalDeviceList(); 435 } 436 437 Object getServiceLock() { 438 return mLock; 439 } 440 441 void setAudioStatus(boolean mute, int volume) { 442 // TODO: Hook up with AudioManager. 443 } 444 445 private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) { 446 // TODO: find better name instead of model name. 447 String displayName = Build.MODEL; 448 return new HdmiCecDeviceInfo(logicalAddress, 449 getPhysicalAddress(), deviceType, getVendorId(), displayName); 450 } 451 452 // Record class that monitors the event of the caller of being killed. Used to clean up 453 // the listener list and record list accordingly. 454 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 455 private final IHdmiHotplugEventListener mListener; 456 457 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 458 mListener = listener; 459 } 460 461 @Override 462 public void binderDied() { 463 synchronized (mLock) { 464 mHotplugEventListenerRecords.remove(this); 465 mHotplugEventListeners.remove(mListener); 466 } 467 } 468 } 469 470 private final class DeviceEventListenerRecord implements IBinder.DeathRecipient { 471 private final IHdmiDeviceEventListener mListener; 472 473 public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) { 474 mListener = listener; 475 } 476 477 @Override 478 public void binderDied() { 479 synchronized (mLock) { 480 mDeviceEventListenerRecords.remove(this); 481 mDeviceEventListeners.remove(mListener); 482 } 483 } 484 } 485 486 private void enforceAccessPermission() { 487 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 488 } 489 490 private final class BinderService extends IHdmiControlService.Stub { 491 @Override 492 public int[] getSupportedTypes() { 493 enforceAccessPermission(); 494 // mLocalDevices is an unmodifiable list - no lock necesary. 495 int[] localDevices = new int[mLocalDevices.size()]; 496 for (int i = 0; i < localDevices.length; ++i) { 497 localDevices[i] = mLocalDevices.get(i); 498 } 499 return localDevices; 500 } 501 502 @Override 503 public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) { 504 enforceAccessPermission(); 505 runOnServiceThread(new Runnable() { 506 @Override 507 public void run() { 508 HdmiCecLocalDeviceTv tv = tv(); 509 if (tv == null) { 510 Slog.w(TAG, "Local tv device not available"); 511 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 512 return; 513 } 514 tv.deviceSelect(logicalAddress, callback); 515 } 516 }); 517 } 518 519 @Override 520 public void portSelect(final int portId, final IHdmiControlCallback callback) { 521 enforceAccessPermission(); 522 runOnServiceThread(new Runnable() { 523 @Override 524 public void run() { 525 HdmiCecLocalDeviceTv tv = tv(); 526 if (tv == null) { 527 Slog.w(TAG, "Local tv device not available"); 528 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 529 return; 530 } 531 tv.portSelect(portId, callback); 532 } 533 }); 534 } 535 536 @Override 537 public void sendKeyEvent(final int keyCode, final boolean isPressed) { 538 enforceAccessPermission(); 539 runOnServiceThread(new Runnable() { 540 @Override 541 public void run() { 542 // TODO: sendKeyEvent is for TV device only for now. Allow other 543 // local devices of different types to use this as well. 544 HdmiCecLocalDeviceTv tv = tv(); 545 if (tv == null) { 546 Slog.w(TAG, "Local tv device not available"); 547 return; 548 } 549 tv.sendKeyEvent(keyCode, isPressed); 550 } 551 }); 552 } 553 554 @Override 555 public void oneTouchPlay(final IHdmiControlCallback callback) { 556 enforceAccessPermission(); 557 runOnServiceThread(new Runnable() { 558 @Override 559 public void run() { 560 HdmiControlService.this.oneTouchPlay(callback); 561 } 562 }); 563 } 564 565 @Override 566 public void queryDisplayStatus(final IHdmiControlCallback callback) { 567 enforceAccessPermission(); 568 runOnServiceThread(new Runnable() { 569 @Override 570 public void run() { 571 HdmiControlService.this.queryDisplayStatus(callback); 572 } 573 }); 574 } 575 576 @Override 577 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 578 enforceAccessPermission(); 579 runOnServiceThread(new Runnable() { 580 @Override 581 public void run() { 582 HdmiControlService.this.addHotplugEventListener(listener); 583 } 584 }); 585 } 586 587 @Override 588 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 589 enforceAccessPermission(); 590 runOnServiceThread(new Runnable() { 591 @Override 592 public void run() { 593 HdmiControlService.this.removeHotplugEventListener(listener); 594 } 595 }); 596 } 597 598 @Override 599 public void addDeviceEventListener(final IHdmiDeviceEventListener listener) { 600 enforceAccessPermission(); 601 runOnServiceThread(new Runnable() { 602 @Override 603 public void run() { 604 HdmiControlService.this.addDeviceEventListener(listener); 605 } 606 }); 607 } 608 609 @Override 610 public List<HdmiPortInfo> getPortInfo() { 611 enforceAccessPermission(); 612 return mPortInfo; 613 } 614 } 615 616 private void oneTouchPlay(final IHdmiControlCallback callback) { 617 assertRunOnServiceThread(); 618 HdmiCecLocalDevicePlayback source = playback(); 619 if (source == null) { 620 Slog.w(TAG, "Local playback device not available"); 621 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 622 return; 623 } 624 source.oneTouchPlay(callback); 625 } 626 627 private void queryDisplayStatus(final IHdmiControlCallback callback) { 628 assertRunOnServiceThread(); 629 HdmiCecLocalDevicePlayback source = playback(); 630 if (source == null) { 631 Slog.w(TAG, "Local playback device not available"); 632 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 633 return; 634 } 635 source.queryDisplayStatus(callback); 636 } 637 638 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 639 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 640 try { 641 listener.asBinder().linkToDeath(record, 0); 642 } catch (RemoteException e) { 643 Slog.w(TAG, "Listener already died"); 644 return; 645 } 646 synchronized (mLock) { 647 mHotplugEventListenerRecords.add(record); 648 mHotplugEventListeners.add(listener); 649 } 650 } 651 652 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 653 synchronized (mLock) { 654 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 655 if (record.mListener.asBinder() == listener.asBinder()) { 656 listener.asBinder().unlinkToDeath(record, 0); 657 mHotplugEventListenerRecords.remove(record); 658 break; 659 } 660 } 661 mHotplugEventListeners.remove(listener); 662 } 663 } 664 665 private void addDeviceEventListener(IHdmiDeviceEventListener listener) { 666 synchronized (mLock) { 667 for (DeviceEventListenerRecord record : mDeviceEventListenerRecords) { 668 if (record.mListener.asBinder() == listener.asBinder()) { 669 listener.asBinder().unlinkToDeath(record, 0); 670 mDeviceEventListenerRecords.remove(record); 671 break; 672 } 673 } 674 mHotplugEventListeners.remove(listener); 675 } 676 } 677 678 private void invokeCallback(IHdmiControlCallback callback, int result) { 679 try { 680 callback.onComplete(result); 681 } catch (RemoteException e) { 682 Slog.e(TAG, "Invoking callback failed:" + e); 683 } 684 } 685 686 private void announceHotplugEvent(int portNo, boolean connected) { 687 HdmiHotplugEvent event = new HdmiHotplugEvent(portNo, connected); 688 synchronized (mLock) { 689 for (IHdmiHotplugEventListener listener : mHotplugEventListeners) { 690 invokeHotplugEventListener(listener, event); 691 } 692 } 693 } 694 695 private void invokeHotplugEventListener(IHdmiHotplugEventListener listener, 696 HdmiHotplugEvent event) { 697 try { 698 listener.onReceived(event); 699 } catch (RemoteException e) { 700 Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e); 701 } 702 } 703 704 private static boolean hasSameTopPort(int path1, int path2) { 705 return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK) 706 == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK); 707 } 708 709 private HdmiCecLocalDeviceTv tv() { 710 return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV); 711 } 712 713 private HdmiCecLocalDevicePlayback playback() { 714 return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK); 715 } 716} 717