HdmiControlService.java revision 09ffc846af78f949d2847003db9f793bfb5eefaa
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.BroadcastReceiver; 21import android.content.ContentResolver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.hardware.hdmi.HdmiCecDeviceInfo; 26import android.hardware.hdmi.HdmiControlManager; 27import android.hardware.hdmi.HdmiHotplugEvent; 28import android.hardware.hdmi.HdmiPortInfo; 29import android.hardware.hdmi.HdmiTvClient; 30import android.hardware.hdmi.IHdmiControlCallback; 31import android.hardware.hdmi.IHdmiControlService; 32import android.hardware.hdmi.IHdmiDeviceEventListener; 33import android.hardware.hdmi.IHdmiHotplugEventListener; 34import android.hardware.hdmi.IHdmiInputChangeListener; 35import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener; 36import android.hardware.hdmi.IHdmiVendorCommandListener; 37import android.media.AudioManager; 38import android.os.Build; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.IBinder; 42import android.os.Looper; 43import android.os.PowerManager; 44import android.os.RemoteException; 45import android.os.SystemClock; 46import android.provider.Settings.Global; 47import android.provider.Settings.SettingNotFoundException; 48import android.util.Slog; 49import android.util.SparseArray; 50import android.util.SparseIntArray; 51 52import com.android.internal.annotations.GuardedBy; 53import com.android.server.SystemService; 54import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 55import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback; 56import com.android.server.hdmi.HdmiCecLocalDevice.PendingActionClearedCallback; 57 58import java.util.ArrayList; 59import java.util.Collections; 60import java.util.List; 61 62/** 63 * Provides a service for sending and processing HDMI control messages, 64 * HDMI-CEC and MHL control command, and providing the information on both standard. 65 */ 66public final class HdmiControlService extends SystemService { 67 private static final String TAG = "HdmiControlService"; 68 69 static final String PERMISSION = "android.permission.HDMI_CEC"; 70 71 /** 72 * Interface to report send result. 73 */ 74 interface SendMessageCallback { 75 /** 76 * Called when {@link HdmiControlService#sendCecCommand} is completed. 77 * 78 * @param error result of send request. 79 * <ul> 80 * <li>{@link Constants#SEND_RESULT_SUCCESS} 81 * <li>{@link Constants#SEND_RESULT_NAK} 82 * <li>{@link Constants#SEND_RESULT_FAILURE} 83 * </ul> 84 */ 85 void onSendCompleted(int error); 86 } 87 88 /** 89 * Interface to get a list of available logical devices. 90 */ 91 interface DevicePollingCallback { 92 /** 93 * Called when device polling is finished. 94 * 95 * @param ackedAddress a list of logical addresses of available devices 96 */ 97 void onPollingFinished(List<Integer> ackedAddress); 98 } 99 100 private class PowerStateReceiver extends BroadcastReceiver { 101 @Override 102 public void onReceive(Context context, Intent intent) { 103 switch (intent.getAction()) { 104 case Intent.ACTION_SCREEN_OFF: 105 if (isPowerOnOrTransient()) { 106 onStandby(); 107 } 108 break; 109 case Intent.ACTION_SCREEN_ON: 110 if (isPowerStandbyOrTransient()) { 111 onWakeUp(); 112 } 113 break; 114 } 115 } 116 } 117 118 // A thread to handle synchronous IO of CEC and MHL control service. 119 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 120 // and sparse call it shares a thread to handle IO operations. 121 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 122 123 // Used to synchronize the access to the service. 124 private final Object mLock = new Object(); 125 126 // Type of logical devices hosted in the system. Stored in the unmodifiable list. 127 private final List<Integer> mLocalDevices; 128 129 // List of listeners registered by callers that want to get notified of 130 // hotplug events. 131 @GuardedBy("mLock") 132 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 133 134 // List of records for hotplug event listener to handle the the caller killed in action. 135 @GuardedBy("mLock") 136 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 137 new ArrayList<>(); 138 139 // List of listeners registered by callers that want to get notified of 140 // device status events. 141 @GuardedBy("mLock") 142 private final ArrayList<IHdmiDeviceEventListener> mDeviceEventListeners = new ArrayList<>(); 143 144 // List of records for device event listener to handle the the caller killed in action. 145 @GuardedBy("mLock") 146 private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords = 147 new ArrayList<>(); 148 149 // List of records for vendor command listener to handle the the caller killed in action. 150 @GuardedBy("mLock") 151 private final ArrayList<VendorCommandListenerRecord> mVendorCommandListenerRecords = 152 new ArrayList<>(); 153 154 @GuardedBy("mLock") 155 private IHdmiInputChangeListener mInputChangeListener; 156 157 @GuardedBy("mLock") 158 private InputChangeListenerRecord mInputChangeListenerRecord; 159 160 // Set to true while HDMI control is enabled. If set to false, HDMI-CEC/MHL protocol 161 // handling will be disabled and no request will be handled. 162 @GuardedBy("mLock") 163 private boolean mHdmiControlEnabled; 164 165 // Set to true while the service is in normal mode. While set to false, no input change is 166 // allowed. Used for situations where input change can confuse users such as channel auto-scan, 167 // system upgrade, etc., a.k.a. "prohibit mode". 168 @GuardedBy("mLock") 169 private boolean mProhibitMode; 170 171 // List of listeners registered by callers that want to get notified of 172 // system audio mode changes. 173 private final ArrayList<IHdmiSystemAudioModeChangeListener> 174 mSystemAudioModeChangeListeners = new ArrayList<>(); 175 // List of records for system audio mode change to handle the the caller killed in action. 176 private final ArrayList<SystemAudioModeChangeListenerRecord> 177 mSystemAudioModeChangeListenerRecords = new ArrayList<>(); 178 179 // Handler used to run a task in service thread. 180 private final Handler mHandler = new Handler(); 181 182 @Nullable 183 private HdmiCecController mCecController; 184 185 @Nullable 186 private HdmiMhlController mMhlController; 187 188 // HDMI port information. Stored in the unmodifiable list to keep the static information 189 // from being modified. 190 private List<HdmiPortInfo> mPortInfo; 191 192 private HdmiCecMessageValidator mMessageValidator; 193 194 private final PowerStateReceiver mPowerStateReceiver = new PowerStateReceiver(); 195 196 @ServiceThreadOnly 197 private int mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY; 198 199 @ServiceThreadOnly 200 private boolean mStandbyMessageReceived = false; 201 202 public HdmiControlService(Context context) { 203 super(context); 204 mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray( 205 com.android.internal.R.array.config_hdmiCecLogicalDeviceType)); 206 } 207 208 @Override 209 public void onStart() { 210 mIoThread.start(); 211 mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; 212 mProhibitMode = false; 213 mHdmiControlEnabled = readBooleanSetting(Global.HDMI_CONTROL_ENABLED, true); 214 215 mCecController = HdmiCecController.create(this); 216 if (mCecController != null) { 217 // TODO: Remove this as soon as OEM's HAL implementation is corrected. 218 mCecController.setOption(HdmiTvClient.OPTION_CEC_ENABLE, 219 HdmiTvClient.ENABLED); 220 221 // TODO: load value for mHdmiControlEnabled from preference. 222 if (mHdmiControlEnabled) { 223 initializeCec(true); 224 } 225 } else { 226 Slog.i(TAG, "Device does not support HDMI-CEC."); 227 } 228 229 mMhlController = HdmiMhlController.create(this); 230 if (mMhlController == null) { 231 Slog.i(TAG, "Device does not support MHL-control."); 232 } 233 mPortInfo = initPortInfo(); 234 mMessageValidator = new HdmiCecMessageValidator(this); 235 publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); 236 237 // Register broadcast receiver for power state change. 238 if (mCecController != null || mMhlController != null) { 239 IntentFilter filter = new IntentFilter(); 240 filter.addAction(Intent.ACTION_SCREEN_OFF); 241 filter.addAction(Intent.ACTION_SCREEN_ON); 242 getContext().registerReceiver(mPowerStateReceiver, filter); 243 } 244 } 245 246 boolean readBooleanSetting(String key, boolean defVal) { 247 ContentResolver cr = getContext().getContentResolver(); 248 return Global.getInt(cr, key, defVal ? Constants.TRUE : Constants.FALSE) == Constants.TRUE; 249 } 250 251 void writeBooleanSetting(String key, boolean value) { 252 ContentResolver cr = getContext().getContentResolver(); 253 Global.putInt(cr, key, value ? Constants.TRUE : Constants.FALSE); 254 } 255 256 private void initializeCec(boolean fromBootup) { 257 mCecController.setOption(HdmiTvClient.OPTION_CEC_SERVICE_CONTROL, 258 HdmiTvClient.ENABLED); 259 initializeLocalDevices(mLocalDevices, fromBootup); 260 } 261 262 @ServiceThreadOnly 263 private void initializeLocalDevices(final List<Integer> deviceTypes, final boolean fromBootup) { 264 assertRunOnServiceThread(); 265 // A container for [Logical Address, Local device info]. 266 final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>(); 267 final SparseIntArray finished = new SparseIntArray(); 268 mCecController.clearLogicalAddress(); 269 for (int type : deviceTypes) { 270 final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type); 271 localDevice.init(); 272 mCecController.allocateLogicalAddress(type, 273 localDevice.getPreferredAddress(), new AllocateAddressCallback() { 274 @Override 275 public void onAllocated(int deviceType, int logicalAddress) { 276 if (logicalAddress == Constants.ADDR_UNREGISTERED) { 277 Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]"); 278 } else { 279 HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType); 280 localDevice.setDeviceInfo(deviceInfo); 281 mCecController.addLocalDevice(deviceType, localDevice); 282 mCecController.addLogicalAddress(logicalAddress); 283 devices.append(logicalAddress, localDevice); 284 } 285 finished.append(deviceType, logicalAddress); 286 287 // Address allocation completed for all devices. Notify each device. 288 if (deviceTypes.size() == finished.size()) { 289 if (mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) { 290 mPowerStatus = HdmiControlManager.POWER_STATUS_ON; 291 } 292 notifyAddressAllocated(devices, fromBootup); 293 } 294 } 295 }); 296 } 297 } 298 299 @ServiceThreadOnly 300 private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices, 301 boolean fromBootup) { 302 assertRunOnServiceThread(); 303 for (int i = 0; i < devices.size(); ++i) { 304 int address = devices.keyAt(i); 305 HdmiCecLocalDevice device = devices.valueAt(i); 306 device.handleAddressAllocated(address, fromBootup); 307 } 308 } 309 310 // Initialize HDMI port information. Combine the information from CEC and MHL HAL and 311 // keep them in one place. 312 @ServiceThreadOnly 313 private List<HdmiPortInfo> initPortInfo() { 314 assertRunOnServiceThread(); 315 HdmiPortInfo[] cecPortInfo = null; 316 317 // CEC HAL provides majority of the info while MHL does only MHL support flag for 318 // each port. Return empty array if CEC HAL didn't provide the info. 319 if (mCecController != null) { 320 cecPortInfo = mCecController.getPortInfos(); 321 } 322 if (cecPortInfo == null) { 323 return Collections.emptyList(); 324 } 325 326 HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0]; 327 if (mMhlController != null) { 328 // TODO: Implement plumbing logic to get MHL port information. 329 // mhlPortInfo = mMhlController.getPortInfos(); 330 } 331 332 // Use the id (port number) to find the matched info between CEC and MHL to combine them 333 // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found. 334 ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length); 335 for (int i = 0; i < cecPortInfo.length; ++i) { 336 HdmiPortInfo cec = cecPortInfo[i]; 337 int id = cec.getId(); 338 boolean mhlInfoFound = false; 339 for (HdmiPortInfo mhl : mhlPortInfo) { 340 if (id == mhl.getId()) { 341 result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(), 342 cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported())); 343 mhlInfoFound = true; 344 break; 345 } 346 } 347 if (!mhlInfoFound) { 348 result.add(cec); 349 } 350 } 351 352 return Collections.unmodifiableList(result); 353 } 354 355 /** 356 * Returns HDMI port information for the given port id. 357 * 358 * @param portId HDMI port id 359 * @return {@link HdmiPortInfo} for the given port 360 */ 361 HdmiPortInfo getPortInfo(int portId) { 362 // mPortInfo is an unmodifiable list and the only reference to its inner list. 363 // No lock is necessary. 364 for (HdmiPortInfo info : mPortInfo) { 365 if (portId == info.getId()) { 366 return info; 367 } 368 } 369 return null; 370 } 371 372 /** 373 * Returns the routing path (physical address) of the HDMI port for the given 374 * port id. 375 */ 376 int portIdToPath(int portId) { 377 HdmiPortInfo portInfo = getPortInfo(portId); 378 if (portInfo == null) { 379 Slog.e(TAG, "Cannot find the port info: " + portId); 380 return Constants.INVALID_PHYSICAL_ADDRESS; 381 } 382 return portInfo.getAddress(); 383 } 384 385 /** 386 * Returns the id of HDMI port located at the top of the hierarchy of 387 * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance, 388 * the port id to be returned is the ID associated with the port address 389 * 0x1000 (1.0.0.0) which is the topmost path of the given routing path. 390 */ 391 int pathToPortId(int path) { 392 int portAddress = path & Constants.ROUTING_PATH_TOP_MASK; 393 for (HdmiPortInfo info : mPortInfo) { 394 if (portAddress == info.getAddress()) { 395 return info.getId(); 396 } 397 } 398 return Constants.INVALID_PORT_ID; 399 } 400 401 boolean isValidPortId(int portId) { 402 for (HdmiPortInfo info : mPortInfo) { 403 if (portId == info.getId()) { 404 return true; 405 } 406 } 407 return false; 408 } 409 410 /** 411 * Returns {@link Looper} for IO operation. 412 * 413 * <p>Declared as package-private. 414 */ 415 Looper getIoLooper() { 416 return mIoThread.getLooper(); 417 } 418 419 /** 420 * Returns {@link Looper} of main thread. Use this {@link Looper} instance 421 * for tasks that are running on main service thread. 422 * 423 * <p>Declared as package-private. 424 */ 425 Looper getServiceLooper() { 426 return mHandler.getLooper(); 427 } 428 429 /** 430 * Returns physical address of the device. 431 */ 432 int getPhysicalAddress() { 433 return mCecController.getPhysicalAddress(); 434 } 435 436 /** 437 * Returns vendor id of CEC service. 438 */ 439 int getVendorId() { 440 return mCecController.getVendorId(); 441 } 442 443 @ServiceThreadOnly 444 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 445 assertRunOnServiceThread(); 446 HdmiCecLocalDeviceTv tv = tv(); 447 if (tv == null) { 448 return null; 449 } 450 return tv.getDeviceInfo(logicalAddress); 451 } 452 453 /** 454 * Returns version of CEC. 455 */ 456 int getCecVersion() { 457 return mCecController.getVersion(); 458 } 459 460 /** 461 * Whether a device of the specified physical address is connected to ARC enabled port. 462 */ 463 boolean isConnectedToArcPort(int physicalAddress) { 464 for (HdmiPortInfo portInfo : mPortInfo) { 465 if (hasSameTopPort(portInfo.getAddress(), physicalAddress) 466 && portInfo.isArcSupported()) { 467 return true; 468 } 469 } 470 return false; 471 } 472 473 void runOnServiceThread(Runnable runnable) { 474 mHandler.post(runnable); 475 } 476 477 void runOnServiceThreadAtFrontOfQueue(Runnable runnable) { 478 mHandler.postAtFrontOfQueue(runnable); 479 } 480 481 private void assertRunOnServiceThread() { 482 if (Looper.myLooper() != mHandler.getLooper()) { 483 throw new IllegalStateException("Should run on service thread."); 484 } 485 } 486 487 /** 488 * Transmit a CEC command to CEC bus. 489 * 490 * @param command CEC command to send out 491 * @param callback interface used to the result of send command 492 */ 493 @ServiceThreadOnly 494 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { 495 assertRunOnServiceThread(); 496 mCecController.sendCommand(command, callback); 497 } 498 499 @ServiceThreadOnly 500 void sendCecCommand(HdmiCecMessage command) { 501 assertRunOnServiceThread(); 502 mCecController.sendCommand(command, null); 503 } 504 505 @ServiceThreadOnly 506 boolean handleCecCommand(HdmiCecMessage message) { 507 assertRunOnServiceThread(); 508 if (!mMessageValidator.isValid(message)) { 509 return false; 510 } 511 return dispatchMessageToLocalDevice(message); 512 } 513 514 void setAudioReturnChannel(boolean enabled) { 515 mCecController.setAudioReturnChannel(enabled); 516 } 517 518 @ServiceThreadOnly 519 private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) { 520 assertRunOnServiceThread(); 521 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 522 if (device.dispatchMessage(message) 523 && message.getDestination() != Constants.ADDR_BROADCAST) { 524 return true; 525 } 526 } 527 528 if (message.getDestination() != Constants.ADDR_BROADCAST) { 529 Slog.w(TAG, "Unhandled cec command:" + message); 530 } 531 return false; 532 } 533 534 /** 535 * Called when a new hotplug event is issued. 536 * 537 * @param portNo hdmi port number where hot plug event issued. 538 * @param connected whether to be plugged in or not 539 */ 540 @ServiceThreadOnly 541 void onHotplug(int portNo, boolean connected) { 542 assertRunOnServiceThread(); 543 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 544 device.onHotplug(portNo, connected); 545 } 546 announceHotplugEvent(portNo, connected); 547 } 548 549 /** 550 * Poll all remote devices. It sends <Polling Message> to all remote 551 * devices. 552 * 553 * @param callback an interface used to get a list of all remote devices' address 554 * @param sourceAddress a logical address of source device where sends polling message 555 * @param pickStrategy strategy how to pick polling candidates 556 * @param retryCount the number of retry used to send polling message to remote devices 557 * @throw IllegalArgumentException if {@code pickStrategy} is invalid value 558 */ 559 @ServiceThreadOnly 560 void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy, 561 int retryCount) { 562 assertRunOnServiceThread(); 563 mCecController.pollDevices(callback, sourceAddress, checkPollStrategy(pickStrategy), 564 retryCount); 565 } 566 567 private int checkPollStrategy(int pickStrategy) { 568 int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK; 569 if (strategy == 0) { 570 throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy); 571 } 572 int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK; 573 if (iterationStrategy == 0) { 574 throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy); 575 } 576 return strategy | iterationStrategy; 577 } 578 579 List<HdmiCecLocalDevice> getAllLocalDevices() { 580 assertRunOnServiceThread(); 581 return mCecController.getLocalDeviceList(); 582 } 583 584 Object getServiceLock() { 585 return mLock; 586 } 587 588 void setAudioStatus(boolean mute, int volume) { 589 AudioManager audioManager = getAudioManager(); 590 boolean muted = audioManager.isStreamMute(AudioManager.STREAM_MUSIC); 591 if (mute) { 592 if (!muted) { 593 audioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); 594 } 595 } else { 596 if (muted) { 597 audioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); 598 } 599 // FLAG_HDMI_SYSTEM_AUDIO_VOLUME prevents audio manager from announcing 600 // volume change notification back to hdmi control service. 601 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 602 AudioManager.FLAG_SHOW_UI | 603 AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME); 604 } 605 } 606 607 void announceSystemAudioModeChange(boolean enabled) { 608 for (IHdmiSystemAudioModeChangeListener listener : mSystemAudioModeChangeListeners) { 609 invokeSystemAudioModeChange(listener, enabled); 610 } 611 } 612 613 private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) { 614 // TODO: find better name instead of model name. 615 String displayName = Build.MODEL; 616 return new HdmiCecDeviceInfo(logicalAddress, 617 getPhysicalAddress(), deviceType, getVendorId(), displayName); 618 } 619 620 // Record class that monitors the event of the caller of being killed. Used to clean up 621 // the listener list and record list accordingly. 622 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 623 private final IHdmiHotplugEventListener mListener; 624 625 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 626 mListener = listener; 627 } 628 629 @Override 630 public void binderDied() { 631 synchronized (mLock) { 632 mHotplugEventListenerRecords.remove(this); 633 mHotplugEventListeners.remove(mListener); 634 } 635 } 636 } 637 638 private final class DeviceEventListenerRecord implements IBinder.DeathRecipient { 639 private final IHdmiDeviceEventListener mListener; 640 641 public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) { 642 mListener = listener; 643 } 644 645 @Override 646 public void binderDied() { 647 synchronized (mLock) { 648 mDeviceEventListenerRecords.remove(this); 649 mDeviceEventListeners.remove(mListener); 650 } 651 } 652 } 653 654 private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient { 655 private final IHdmiSystemAudioModeChangeListener mListener; 656 657 public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) { 658 mListener = listener; 659 } 660 661 @Override 662 public void binderDied() { 663 synchronized (mLock) { 664 mSystemAudioModeChangeListenerRecords.remove(this); 665 mSystemAudioModeChangeListeners.remove(mListener); 666 } 667 } 668 } 669 670 class VendorCommandListenerRecord implements IBinder.DeathRecipient { 671 private final IHdmiVendorCommandListener mListener; 672 private final int mDeviceType; 673 674 public VendorCommandListenerRecord(IHdmiVendorCommandListener listener, int deviceType) { 675 mListener = listener; 676 mDeviceType = deviceType; 677 } 678 679 @Override 680 public void binderDied() { 681 synchronized (mLock) { 682 mVendorCommandListenerRecords.remove(this); 683 } 684 } 685 } 686 687 private void enforceAccessPermission() { 688 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 689 } 690 691 private final class BinderService extends IHdmiControlService.Stub { 692 @Override 693 public int[] getSupportedTypes() { 694 enforceAccessPermission(); 695 // mLocalDevices is an unmodifiable list - no lock necesary. 696 int[] localDevices = new int[mLocalDevices.size()]; 697 for (int i = 0; i < localDevices.length; ++i) { 698 localDevices[i] = mLocalDevices.get(i); 699 } 700 return localDevices; 701 } 702 703 @Override 704 public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) { 705 enforceAccessPermission(); 706 runOnServiceThread(new Runnable() { 707 @Override 708 public void run() { 709 HdmiCecLocalDeviceTv tv = tv(); 710 if (tv == null) { 711 Slog.w(TAG, "Local tv device not available"); 712 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 713 return; 714 } 715 tv.deviceSelect(logicalAddress, callback); 716 } 717 }); 718 } 719 720 @Override 721 public void portSelect(final int portId, final IHdmiControlCallback callback) { 722 enforceAccessPermission(); 723 runOnServiceThread(new Runnable() { 724 @Override 725 public void run() { 726 HdmiCecLocalDeviceTv tv = tv(); 727 if (tv == null) { 728 Slog.w(TAG, "Local tv device not available"); 729 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 730 return; 731 } 732 tv.doManualPortSwitching(portId, callback); 733 } 734 }); 735 } 736 737 @Override 738 public void sendKeyEvent(final int deviceType, final int keyCode, final boolean isPressed) { 739 enforceAccessPermission(); 740 runOnServiceThread(new Runnable() { 741 @Override 742 public void run() { 743 HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(deviceType); 744 if (localDevice == null) { 745 Slog.w(TAG, "Local device not available"); 746 return; 747 } 748 localDevice.sendKeyEvent(keyCode, isPressed); 749 } 750 }); 751 } 752 753 @Override 754 public void oneTouchPlay(final IHdmiControlCallback callback) { 755 enforceAccessPermission(); 756 runOnServiceThread(new Runnable() { 757 @Override 758 public void run() { 759 HdmiControlService.this.oneTouchPlay(callback); 760 } 761 }); 762 } 763 764 @Override 765 public void queryDisplayStatus(final IHdmiControlCallback callback) { 766 enforceAccessPermission(); 767 runOnServiceThread(new Runnable() { 768 @Override 769 public void run() { 770 HdmiControlService.this.queryDisplayStatus(callback); 771 } 772 }); 773 } 774 775 @Override 776 public void addHotplugEventListener(final IHdmiHotplugEventListener listener) { 777 enforceAccessPermission(); 778 runOnServiceThread(new Runnable() { 779 @Override 780 public void run() { 781 HdmiControlService.this.addHotplugEventListener(listener); 782 } 783 }); 784 } 785 786 @Override 787 public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) { 788 enforceAccessPermission(); 789 runOnServiceThread(new Runnable() { 790 @Override 791 public void run() { 792 HdmiControlService.this.removeHotplugEventListener(listener); 793 } 794 }); 795 } 796 797 @Override 798 public void addDeviceEventListener(final IHdmiDeviceEventListener listener) { 799 enforceAccessPermission(); 800 runOnServiceThread(new Runnable() { 801 @Override 802 public void run() { 803 HdmiControlService.this.addDeviceEventListener(listener); 804 } 805 }); 806 } 807 808 @Override 809 public List<HdmiPortInfo> getPortInfo() { 810 enforceAccessPermission(); 811 return mPortInfo; 812 } 813 814 @Override 815 public boolean canChangeSystemAudioMode() { 816 enforceAccessPermission(); 817 HdmiCecLocalDeviceTv tv = tv(); 818 if (tv == null) { 819 return false; 820 } 821 return tv.hasSystemAudioDevice(); 822 } 823 824 @Override 825 public boolean getSystemAudioMode() { 826 enforceAccessPermission(); 827 HdmiCecLocalDeviceTv tv = tv(); 828 if (tv == null) { 829 return false; 830 } 831 return tv.getSystemAudioMode(); 832 } 833 834 @Override 835 public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) { 836 enforceAccessPermission(); 837 runOnServiceThread(new Runnable() { 838 @Override 839 public void run() { 840 HdmiCecLocalDeviceTv tv = tv(); 841 if (tv == null) { 842 Slog.w(TAG, "Local tv device not available"); 843 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 844 return; 845 } 846 tv.changeSystemAudioMode(enabled, callback); 847 } 848 }); 849 } 850 851 @Override 852 public void addSystemAudioModeChangeListener( 853 final IHdmiSystemAudioModeChangeListener listener) { 854 enforceAccessPermission(); 855 HdmiControlService.this.addSystemAudioModeChangeListner(listener); 856 } 857 858 @Override 859 public void removeSystemAudioModeChangeListener( 860 final IHdmiSystemAudioModeChangeListener listener) { 861 enforceAccessPermission(); 862 HdmiControlService.this.removeSystemAudioModeChangeListener(listener); 863 } 864 865 @Override 866 public void setInputChangeListener(final IHdmiInputChangeListener listener) { 867 enforceAccessPermission(); 868 HdmiControlService.this.setInputChangeListener(listener); 869 } 870 871 @Override 872 public List<HdmiCecDeviceInfo> getInputDevices() { 873 enforceAccessPermission(); 874 // No need to hold the lock for obtaining TV device as the local device instance 875 // is preserved while the HDMI control is enabled. 876 HdmiCecLocalDeviceTv tv = tv(); 877 if (tv == null) { 878 return Collections.emptyList(); 879 } 880 return tv.getSafeExternalInputs(); 881 } 882 883 @Override 884 public void setControlEnabled(final boolean enabled) { 885 enforceAccessPermission(); 886 runOnServiceThread(new Runnable() { 887 @Override 888 public void run() { 889 handleHdmiControlStatusChanged(enabled); 890 891 } 892 }); 893 } 894 895 @Override 896 public void setSystemAudioVolume(final int oldIndex, final int newIndex, 897 final int maxIndex) { 898 enforceAccessPermission(); 899 runOnServiceThread(new Runnable() { 900 @Override 901 public void run() { 902 HdmiCecLocalDeviceTv tv = tv(); 903 if (tv == null) { 904 Slog.w(TAG, "Local tv device not available"); 905 return; 906 } 907 tv.changeVolume(oldIndex, newIndex - oldIndex, maxIndex); 908 } 909 }); 910 } 911 912 @Override 913 public void setSystemAudioMute(final boolean mute) { 914 enforceAccessPermission(); 915 runOnServiceThread(new Runnable() { 916 @Override 917 public void run() { 918 HdmiCecLocalDeviceTv tv = tv(); 919 if (tv == null) { 920 Slog.w(TAG, "Local tv device not available"); 921 return; 922 } 923 tv.changeMute(mute); 924 } 925 }); 926 } 927 928 @Override 929 public void setArcMode(final boolean enabled) { 930 enforceAccessPermission(); 931 runOnServiceThread(new Runnable() { 932 @Override 933 public void run() { 934 HdmiCecLocalDeviceTv tv = tv(); 935 if (tv == null) { 936 Slog.w(TAG, "Local tv device not available to change arc mode."); 937 return; 938 } 939 } 940 }); 941 } 942 943 @Override 944 public void setOption(final int key, final int value) { 945 enforceAccessPermission(); 946 if (!isTvDevice()) { 947 return; 948 } 949 switch (key) { 950 case HdmiTvClient.OPTION_CEC_AUTO_WAKEUP: 951 mCecController.setOption(key, value); 952 break; 953 case HdmiTvClient.OPTION_CEC_AUTO_DEVICE_OFF: 954 // No need to pass this option to HAL. 955 tv().setAutoDeviceOff(value == HdmiTvClient.ENABLED); 956 break; 957 case HdmiTvClient.OPTION_MHL_INPUT_SWITCHING: // Fall through 958 case HdmiTvClient.OPTION_MHL_POWER_CHARGE: 959 if (mMhlController != null) { 960 mMhlController.setOption(key, value); 961 } 962 break; 963 } 964 } 965 966 private boolean isTvDevice() { 967 return tv() != null; 968 } 969 970 @Override 971 public void setProhibitMode(final boolean enabled) { 972 enforceAccessPermission(); 973 if (!isTvDevice()) { 974 return; 975 } 976 HdmiControlService.this.setProhibitMode(enabled); 977 } 978 979 @Override 980 public void addVendorCommandListener(final IHdmiVendorCommandListener listener, 981 final int deviceType) { 982 enforceAccessPermission(); 983 runOnServiceThread(new Runnable() { 984 @Override 985 public void run() { 986 HdmiControlService.this.addVendorCommandListener(listener, deviceType); 987 } 988 }); 989 } 990 991 @Override 992 public void sendVendorCommand(final int deviceType, final int targetAddress, 993 final byte[] params, final boolean hasVendorId) { 994 enforceAccessPermission(); 995 runOnServiceThread(new Runnable() { 996 @Override 997 public void run() { 998 HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType); 999 if (device == null) { 1000 Slog.w(TAG, "Local device not available"); 1001 return; 1002 } 1003 if (hasVendorId) { 1004 sendCecCommand(HdmiCecMessageBuilder.buildVendorCommandWithId( 1005 device.getDeviceInfo().getLogicalAddress(), targetAddress, 1006 getVendorId(), params)); 1007 } else { 1008 sendCecCommand(HdmiCecMessageBuilder.buildVendorCommand( 1009 device.getDeviceInfo().getLogicalAddress(), targetAddress, params)); 1010 } 1011 } 1012 }); 1013 } 1014 } 1015 1016 @ServiceThreadOnly 1017 private void oneTouchPlay(final IHdmiControlCallback callback) { 1018 assertRunOnServiceThread(); 1019 HdmiCecLocalDevicePlayback source = playback(); 1020 if (source == null) { 1021 Slog.w(TAG, "Local playback device not available"); 1022 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 1023 return; 1024 } 1025 source.oneTouchPlay(callback); 1026 } 1027 1028 @ServiceThreadOnly 1029 private void queryDisplayStatus(final IHdmiControlCallback callback) { 1030 assertRunOnServiceThread(); 1031 HdmiCecLocalDevicePlayback source = playback(); 1032 if (source == null) { 1033 Slog.w(TAG, "Local playback device not available"); 1034 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 1035 return; 1036 } 1037 source.queryDisplayStatus(callback); 1038 } 1039 1040 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 1041 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 1042 try { 1043 listener.asBinder().linkToDeath(record, 0); 1044 } catch (RemoteException e) { 1045 Slog.w(TAG, "Listener already died"); 1046 return; 1047 } 1048 synchronized (mLock) { 1049 mHotplugEventListenerRecords.add(record); 1050 mHotplugEventListeners.add(listener); 1051 } 1052 } 1053 1054 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 1055 synchronized (mLock) { 1056 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 1057 if (record.mListener.asBinder() == listener.asBinder()) { 1058 listener.asBinder().unlinkToDeath(record, 0); 1059 mHotplugEventListenerRecords.remove(record); 1060 break; 1061 } 1062 } 1063 mHotplugEventListeners.remove(listener); 1064 } 1065 } 1066 1067 private void addDeviceEventListener(IHdmiDeviceEventListener listener) { 1068 DeviceEventListenerRecord record = new DeviceEventListenerRecord(listener); 1069 try { 1070 listener.asBinder().linkToDeath(record, 0); 1071 } catch (RemoteException e) { 1072 Slog.w(TAG, "Listener already died"); 1073 return; 1074 } 1075 synchronized (mLock) { 1076 mDeviceEventListeners.add(listener); 1077 mDeviceEventListenerRecords.add(record); 1078 } 1079 } 1080 1081 void invokeDeviceEventListeners(HdmiCecDeviceInfo device, boolean activated) { 1082 synchronized (mLock) { 1083 for (IHdmiDeviceEventListener listener : mDeviceEventListeners) { 1084 try { 1085 listener.onStatusChanged(device, activated); 1086 } catch (RemoteException e) { 1087 Slog.e(TAG, "Failed to report device event:" + e); 1088 } 1089 } 1090 } 1091 } 1092 1093 private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) { 1094 SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord( 1095 listener); 1096 try { 1097 listener.asBinder().linkToDeath(record, 0); 1098 } catch (RemoteException e) { 1099 Slog.w(TAG, "Listener already died"); 1100 return; 1101 } 1102 synchronized (mLock) { 1103 mSystemAudioModeChangeListeners.add(listener); 1104 mSystemAudioModeChangeListenerRecords.add(record); 1105 } 1106 } 1107 1108 private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) { 1109 synchronized (mLock) { 1110 for (SystemAudioModeChangeListenerRecord record : 1111 mSystemAudioModeChangeListenerRecords) { 1112 if (record.mListener.asBinder() == listener) { 1113 listener.asBinder().unlinkToDeath(record, 0); 1114 mSystemAudioModeChangeListenerRecords.remove(record); 1115 break; 1116 } 1117 } 1118 mSystemAudioModeChangeListeners.remove(listener); 1119 } 1120 } 1121 1122 private final class InputChangeListenerRecord implements IBinder.DeathRecipient { 1123 @Override 1124 public void binderDied() { 1125 synchronized (mLock) { 1126 mInputChangeListener = null; 1127 } 1128 } 1129 } 1130 1131 private void setInputChangeListener(IHdmiInputChangeListener listener) { 1132 synchronized (mLock) { 1133 mInputChangeListenerRecord = new InputChangeListenerRecord(); 1134 try { 1135 listener.asBinder().linkToDeath(mInputChangeListenerRecord, 0); 1136 } catch (RemoteException e) { 1137 Slog.w(TAG, "Listener already died"); 1138 return; 1139 } 1140 mInputChangeListener = listener; 1141 } 1142 } 1143 1144 void invokeInputChangeListener(int activeAddress) { 1145 synchronized (mLock) { 1146 if (mInputChangeListener != null) { 1147 HdmiCecDeviceInfo activeSource = getDeviceInfo(activeAddress); 1148 try { 1149 mInputChangeListener.onChanged(activeSource); 1150 } catch (RemoteException e) { 1151 Slog.w(TAG, "Exception thrown by IHdmiInputChangeListener: " + e); 1152 } 1153 } 1154 } 1155 } 1156 1157 private void invokeCallback(IHdmiControlCallback callback, int result) { 1158 try { 1159 callback.onComplete(result); 1160 } catch (RemoteException e) { 1161 Slog.e(TAG, "Invoking callback failed:" + e); 1162 } 1163 } 1164 1165 private void invokeSystemAudioModeChange(IHdmiSystemAudioModeChangeListener listener, 1166 boolean enabled) { 1167 try { 1168 listener.onStatusChanged(enabled); 1169 } catch (RemoteException e) { 1170 Slog.e(TAG, "Invoking callback failed:" + e); 1171 } 1172 } 1173 1174 private void announceHotplugEvent(int portId, boolean connected) { 1175 HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected); 1176 synchronized (mLock) { 1177 for (IHdmiHotplugEventListener listener : mHotplugEventListeners) { 1178 invokeHotplugEventListenerLocked(listener, event); 1179 } 1180 } 1181 } 1182 1183 private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener, 1184 HdmiHotplugEvent event) { 1185 try { 1186 listener.onReceived(event); 1187 } catch (RemoteException e) { 1188 Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e); 1189 } 1190 } 1191 1192 private static boolean hasSameTopPort(int path1, int path2) { 1193 return (path1 & Constants.ROUTING_PATH_TOP_MASK) 1194 == (path2 & Constants.ROUTING_PATH_TOP_MASK); 1195 } 1196 1197 private HdmiCecLocalDeviceTv tv() { 1198 return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCecDeviceInfo.DEVICE_TV); 1199 } 1200 1201 private HdmiCecLocalDevicePlayback playback() { 1202 return (HdmiCecLocalDevicePlayback) 1203 mCecController.getLocalDevice(HdmiCecDeviceInfo.DEVICE_PLAYBACK); 1204 } 1205 1206 AudioManager getAudioManager() { 1207 return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); 1208 } 1209 1210 boolean isControlEnabled() { 1211 synchronized (mLock) { 1212 return mHdmiControlEnabled; 1213 } 1214 } 1215 1216 int getPowerStatus() { 1217 return mPowerStatus; 1218 } 1219 1220 boolean isPowerOnOrTransient() { 1221 return mPowerStatus == HdmiControlManager.POWER_STATUS_ON 1222 || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; 1223 } 1224 1225 boolean isPowerStandbyOrTransient() { 1226 return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY 1227 || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY; 1228 } 1229 1230 boolean isPowerStandby() { 1231 return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY; 1232 } 1233 1234 @ServiceThreadOnly 1235 void wakeUp() { 1236 assertRunOnServiceThread(); 1237 PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); 1238 pm.wakeUp(SystemClock.uptimeMillis()); 1239 // PowerManger will send the broadcast Intent.ACTION_SCREEN_ON and after this gets 1240 // the intent, the sequence will continue at onWakeUp(). 1241 } 1242 1243 @ServiceThreadOnly 1244 void standby() { 1245 assertRunOnServiceThread(); 1246 mStandbyMessageReceived = true; 1247 PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); 1248 pm.goToSleep(SystemClock.uptimeMillis()); 1249 // PowerManger will send the broadcast Intent.ACTION_SCREEN_OFF and after this gets 1250 // the intent, the sequence will continue at onStandby(). 1251 } 1252 1253 @ServiceThreadOnly 1254 private void onWakeUp() { 1255 assertRunOnServiceThread(); 1256 mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON; 1257 if (mCecController != null) { 1258 if (mHdmiControlEnabled) { 1259 initializeCec(true); 1260 } 1261 } else { 1262 Slog.i(TAG, "Device does not support HDMI-CEC."); 1263 } 1264 // TODO: Initialize MHL local devices. 1265 } 1266 1267 @ServiceThreadOnly 1268 private void onStandby() { 1269 assertRunOnServiceThread(); 1270 mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY; 1271 1272 final List<HdmiCecLocalDevice> devices = getAllLocalDevices(); 1273 disableDevices(new PendingActionClearedCallback() { 1274 @Override 1275 public void onCleared(HdmiCecLocalDevice device) { 1276 Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType); 1277 devices.remove(device); 1278 if (devices.isEmpty()) { 1279 clearLocalDevices(); 1280 onStandbyCompleted(); 1281 } 1282 } 1283 }); 1284 } 1285 1286 private void disableDevices(PendingActionClearedCallback callback) { 1287 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 1288 device.disableDevice(mStandbyMessageReceived, callback); 1289 } 1290 } 1291 1292 @ServiceThreadOnly 1293 private void clearLocalDevices() { 1294 assertRunOnServiceThread(); 1295 if (mCecController == null) { 1296 return; 1297 } 1298 mCecController.clearLogicalAddress(); 1299 mCecController.clearLocalDevices(); 1300 } 1301 1302 @ServiceThreadOnly 1303 private void onStandbyCompleted() { 1304 assertRunOnServiceThread(); 1305 Slog.v(TAG, "onStandbyCompleted"); 1306 1307 if (mPowerStatus != HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) { 1308 return; 1309 } 1310 mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY; 1311 for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) { 1312 device.onStandby(mStandbyMessageReceived); 1313 } 1314 mStandbyMessageReceived = false; 1315 mCecController.setOption(HdmiTvClient.OPTION_CEC_SERVICE_CONTROL, HdmiTvClient.DISABLED); 1316 } 1317 1318 private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) { 1319 VendorCommandListenerRecord record = new VendorCommandListenerRecord(listener, deviceType); 1320 try { 1321 listener.asBinder().linkToDeath(record, 0); 1322 } catch (RemoteException e) { 1323 Slog.w(TAG, "Listener already died"); 1324 return; 1325 } 1326 synchronized (mLock) { 1327 mVendorCommandListenerRecords.add(record); 1328 } 1329 } 1330 1331 void invokeVendorCommandListeners(int deviceType, int srcAddress, byte[] params, 1332 boolean hasVendorId) { 1333 synchronized (mLock) { 1334 for (VendorCommandListenerRecord record : mVendorCommandListenerRecords) { 1335 if (record.mDeviceType != deviceType) { 1336 continue; 1337 } 1338 try { 1339 record.mListener.onReceived(srcAddress, params, hasVendorId); 1340 } catch (RemoteException e) { 1341 Slog.e(TAG, "Failed to notify vendor command reception", e); 1342 } 1343 } 1344 } 1345 } 1346 1347 boolean isProhibitMode() { 1348 synchronized (mLock) { 1349 return mProhibitMode; 1350 } 1351 } 1352 1353 void setProhibitMode(boolean enabled) { 1354 synchronized (mLock) { 1355 mProhibitMode = enabled; 1356 } 1357 } 1358 1359 @ServiceThreadOnly 1360 private void handleHdmiControlStatusChanged(boolean enabled) { 1361 assertRunOnServiceThread(); 1362 1363 int value = enabled ? HdmiTvClient.ENABLED : HdmiTvClient.DISABLED; 1364 mCecController.setOption(HdmiTvClient.OPTION_CEC_ENABLE, value); 1365 if (mMhlController != null) { 1366 mMhlController.setOption(HdmiTvClient.OPTION_MHL_ENABLE, value); 1367 } 1368 1369 synchronized (mLock) { 1370 mHdmiControlEnabled = enabled; 1371 } 1372 1373 if (enabled) { 1374 initializeCec(false); 1375 } else { 1376 disableDevices(new PendingActionClearedCallback() { 1377 @Override 1378 public void onCleared(HdmiCecLocalDevice device) { 1379 assertRunOnServiceThread(); 1380 clearLocalDevices(); 1381 } 1382 }); 1383 } 1384 } 1385} 1386