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