HdmiCecLocalDeviceTv.java revision 8fa36b110be29d92a9aba070fa4666eefb14b584
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.hardware.hdmi.HdmiCec; 20import android.hardware.hdmi.HdmiCecDeviceInfo; 21import android.hardware.hdmi.HdmiCecMessage; 22import android.hardware.hdmi.IHdmiControlCallback; 23import android.media.AudioSystem; 24import android.os.RemoteException; 25import android.util.Slog; 26import android.util.SparseArray; 27 28import com.android.internal.annotations.GuardedBy; 29import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 30import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 31 32import java.util.ArrayList; 33import java.util.Collections; 34import java.util.List; 35import java.util.Locale; 36 37/** 38 * Represent a logical device of type TV residing in Android system. 39 */ 40final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 41 private static final String TAG = "HdmiCecLocalDeviceTv"; 42 43 // Whether ARC is "enabled" or not. 44 @GuardedBy("mLock") 45 private boolean mArcStatusEnabled = false; 46 47 // Whether SystemAudioMode is "On" or not. 48 @GuardedBy("mLock") 49 private boolean mSystemAudioMode; 50 51 // The previous port id (input) before switching to the new one. This is remembered in order to 52 // be able to switch to it upon receiving <Inactive Source> from currently active source. 53 // This remains valid only when the active source was switched via one touch play operation 54 // (either by TV or source device). Manual port switching invalidates this value to 55 // HdmiConstants.PORT_INVALID, for which case <Inactive Source> does not do anything. 56 @GuardedBy("mLock") 57 private int mPrevPortId; 58 59 @GuardedBy("mLock") 60 private int mSystemAudioVolume = HdmiConstants.UNKNOWN_VOLUME; 61 62 @GuardedBy("mLock") 63 private boolean mSystemAudioMute = false; 64 65 // Copy of mDeviceInfos to guarantee thread-safety. 66 @GuardedBy("mLock") 67 private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 68 // All external cec device which excludes local devices. 69 @GuardedBy("mLock") 70 private List<HdmiCecDeviceInfo> mSafeExternalDeviceInfos = Collections.emptyList(); 71 72 // Map-like container of all cec devices including local ones. 73 // A logical address of device is used as key of container. 74 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 75 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 76 77 HdmiCecLocalDeviceTv(HdmiControlService service) { 78 super(service, HdmiCec.DEVICE_TV); 79 mPrevPortId = HdmiConstants.INVALID_PORT_ID; 80 // TODO: load system audio mode and set it to mSystemAudioMode. 81 } 82 83 @Override 84 @ServiceThreadOnly 85 protected void onAddressAllocated(int logicalAddress) { 86 assertRunOnServiceThread(); 87 // TODO: vendor-specific initialization here. 88 89 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 90 mAddress, mService.getPhysicalAddress(), mDeviceType)); 91 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 92 mAddress, mService.getVendorId())); 93 94 launchDeviceDiscovery(); 95 // TODO: Start routing control action 96 } 97 98 /** 99 * Performs the action 'device select', or 'one touch play' initiated by TV. 100 * 101 * @param targetAddress logical address of the device to select 102 * @param callback callback object to report the result with 103 */ 104 @ServiceThreadOnly 105 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 106 assertRunOnServiceThread(); 107 if (targetAddress == HdmiCec.ADDR_INTERNAL) { 108 handleSelectInternalSource(callback); 109 return; 110 } 111 HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress); 112 if (targetDevice == null) { 113 invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE); 114 return; 115 } 116 removeAction(DeviceSelectAction.class); 117 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 118 } 119 120 @ServiceThreadOnly 121 private void handleSelectInternalSource(IHdmiControlCallback callback) { 122 assertRunOnServiceThread(); 123 // Seq #18 124 if (isHdmiControlEnabled() && getActiveSource() != mAddress) { 125 updateActiveSource(mAddress, mService.getPhysicalAddress()); 126 // TODO: Check if this comes from <Text/Image View On> - if true, do nothing. 127 HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource( 128 mAddress, mService.getPhysicalAddress()); 129 mService.sendCecCommand(activeSource); 130 } 131 } 132 133 @ServiceThreadOnly 134 void updateActiveSource(int activeSource, int activePath) { 135 assertRunOnServiceThread(); 136 // Seq #14 137 if (activeSource == getActiveSource() && activePath == getActivePath()) { 138 return; 139 } 140 setActiveSource(activeSource); 141 setActivePath(activePath); 142 if (getDeviceInfo(activeSource) != null && activeSource != mAddress) { 143 if (mService.pathToPortId(activePath) == getActivePortId()) { 144 setPrevPortId(getActivePortId()); 145 } 146 // TODO: Show the OSD banner related to the new active source device. 147 } else { 148 // TODO: If displayed, remove the OSD banner related to the previous 149 // active source device. 150 } 151 } 152 153 /** 154 * Returns the previous port id kept to handle input switching on <Inactive Source>. 155 */ 156 int getPrevPortId() { 157 synchronized (mLock) { 158 return mPrevPortId; 159 } 160 } 161 162 /** 163 * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be 164 * taken for <Inactive Source>. 165 */ 166 void setPrevPortId(int portId) { 167 synchronized (mLock) { 168 mPrevPortId = portId; 169 } 170 } 171 172 @ServiceThreadOnly 173 void updateActivePortId(int portId) { 174 assertRunOnServiceThread(); 175 // Seq #15 176 if (portId == getActivePortId()) { 177 return; 178 } 179 setPrevPortId(portId); 180 // TODO: Actually switch the physical port here. Handle PAP/PIP as well. 181 // Show OSD port change banner 182 } 183 184 @ServiceThreadOnly 185 void doManualPortSwitching(int portId, IHdmiControlCallback callback) { 186 assertRunOnServiceThread(); 187 // Seq #20 188 if (!isHdmiControlEnabled() || portId == getActivePortId()) { 189 invokeCallback(callback, HdmiCec.RESULT_INCORRECT_MODE); 190 return; 191 } 192 // TODO: Make sure this call does not stem from <Active Source> message reception. 193 194 setActivePortId(portId); 195 // TODO: Return immediately if the operation is triggered by <Text/Image View On> 196 // and this is the first notification about the active input after power-on. 197 // TODO: Handle invalid port id / active input which should be treated as an 198 // internal tuner. 199 200 removeAction(RoutingControlAction.class); 201 202 int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId())); 203 int newPath = mService.portIdToPath(portId); 204 HdmiCecMessage routingChange = 205 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 206 mService.sendCecCommand(routingChange); 207 addAndStartAction(new RoutingControlAction(this, newPath, callback)); 208 } 209 210 /** 211 * Sends key to a target CEC device. 212 * 213 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 214 * @param isPressed true if this is keypress event 215 */ 216 @ServiceThreadOnly 217 void sendKeyEvent(int keyCode, boolean isPressed) { 218 assertRunOnServiceThread(); 219 List<SendKeyAction> action = getActions(SendKeyAction.class); 220 if (!action.isEmpty()) { 221 action.get(0).processKeyEvent(keyCode, isPressed); 222 } else { 223 if (isPressed) { 224 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 225 } else { 226 Slog.w(TAG, "Discard key release event"); 227 } 228 } 229 } 230 231 private static void invokeCallback(IHdmiControlCallback callback, int result) { 232 if (callback == null) { 233 return; 234 } 235 try { 236 callback.onComplete(result); 237 } catch (RemoteException e) { 238 Slog.e(TAG, "Invoking callback failed:" + e); 239 } 240 } 241 242 @Override 243 @ServiceThreadOnly 244 protected boolean handleActiveSource(HdmiCecMessage message) { 245 assertRunOnServiceThread(); 246 int activePath = HdmiUtils.twoBytesToInt(message.getParams()); 247 ActiveSourceHandler.create(this, null).process(message.getSource(), activePath); 248 return true; 249 } 250 251 @Override 252 @ServiceThreadOnly 253 protected boolean handleInactiveSource(HdmiCecMessage message) { 254 assertRunOnServiceThread(); 255 // Seq #10 256 257 // Ignore <Inactive Source> from non-active source device. 258 if (getActiveSource() != message.getSource()) { 259 return true; 260 } 261 if (isInPresetInstallationMode()) { 262 return true; 263 } 264 int portId = getPrevPortId(); 265 if (portId != HdmiConstants.INVALID_PORT_ID) { 266 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 267 268 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 269 if (inactiveSource == null) { 270 return true; 271 } 272 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 273 return true; 274 } 275 // TODO: Switch the TV freeze mode off 276 277 setActivePortId(portId); 278 doManualPortSwitching(portId, null); 279 setPrevPortId(HdmiConstants.INVALID_PORT_ID); 280 } 281 return true; 282 } 283 284 @Override 285 @ServiceThreadOnly 286 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 287 assertRunOnServiceThread(); 288 // Seq #19 289 int address = getDeviceInfo().getLogicalAddress(); 290 if (address == getActiveSource()) { 291 mService.sendCecCommand( 292 HdmiCecMessageBuilder.buildActiveSource(address, getActivePath())); 293 } 294 return true; 295 } 296 297 @Override 298 @ServiceThreadOnly 299 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 300 assertRunOnServiceThread(); 301 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 302 mAddress, Locale.getDefault().getISO3Language()); 303 // TODO: figure out how to handle failed to get language code. 304 if (command != null) { 305 mService.sendCecCommand(command); 306 } else { 307 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 308 } 309 return true; 310 } 311 312 @Override 313 @ServiceThreadOnly 314 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 315 assertRunOnServiceThread(); 316 // Ignore if [Device Discovery Action] is going on. 317 if (hasAction(DeviceDiscoveryAction.class)) { 318 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 319 + "because Device Discovery Action is on-going:" + message); 320 return true; 321 } 322 323 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 324 int logicalAddress = message.getSource(); 325 326 // If it is a new device and connected to the tail of active path, 327 // it's required to change routing path. 328 boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress) 329 && isTailOfActivePath(physicalAddress); 330 addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress, 331 requireRoutingChange)); 332 return true; 333 } 334 335 @Override 336 @ServiceThreadOnly 337 protected boolean handleVendorSpecificCommand(HdmiCecMessage message) { 338 assertRunOnServiceThread(); 339 List<VendorSpecificAction> actions = Collections.emptyList(); 340 // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions. 341 342 // We assume that there can be multiple vendor-specific command actions running 343 // at the same time. Pass the message to each action to see if one of them needs it. 344 for (VendorSpecificAction action : actions) { 345 if (action.processCommand(message)) { 346 return true; 347 } 348 } 349 // Handle the message here if it is not already consumed by one of the running actions. 350 // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another 351 // vendor-specific action: 352 // 353 // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress)); 354 // 355 // For now, simply reply with <Feature Abort> and mark it consumed by returning true. 356 mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand( 357 message.getDestination(), message.getSource(), message.getOpcode(), 358 HdmiConstants.ABORT_REFUSED)); 359 return true; 360 } 361 362 @Override 363 @ServiceThreadOnly 364 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 365 assertRunOnServiceThread(); 366 367 byte params[] = message.getParams(); 368 if (params.length < 1) { 369 Slog.w(TAG, "Invalide <Report Audio Status> message:" + message); 370 return true; 371 } 372 int mute = params[0] & 0x80; 373 int volume = params[0] & 0x7F; 374 setAudioStatus(mute == 0x80, volume); 375 return true; 376 } 377 378 @ServiceThreadOnly 379 private void launchDeviceDiscovery() { 380 assertRunOnServiceThread(); 381 clearDeviceInfoList(); 382 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 383 new DeviceDiscoveryCallback() { 384 @Override 385 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 386 for (HdmiCecDeviceInfo info : deviceInfos) { 387 addCecDevice(info); 388 } 389 390 // Since we removed all devices when it's start and 391 // device discovery action does not poll local devices, 392 // we should put device info of local device manually here 393 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 394 addCecDevice(device.getDeviceInfo()); 395 } 396 397 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 398 399 // If there is AVR, initiate System Audio Auto initiation action, 400 // which turns on and off system audio according to last system 401 // audio setting. 402 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 403 if (avrInfo != null) { 404 addAndStartAction(new SystemAudioAutoInitiationAction( 405 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 406 } 407 } 408 }); 409 addAndStartAction(action); 410 } 411 412 // Clear all device info. 413 @ServiceThreadOnly 414 private void clearDeviceInfoList() { 415 assertRunOnServiceThread(); 416 mDeviceInfos.clear(); 417 updateSafeDeviceInfoList(); 418 } 419 420 @ServiceThreadOnly 421 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 422 assertRunOnServiceThread(); 423 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 424 if (avr == null) { 425 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 426 return; 427 } 428 429 addAndStartAction( 430 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 431 } 432 433 void setSystemAudioMode(boolean on) { 434 synchronized (mLock) { 435 if (on != mSystemAudioMode) { 436 mSystemAudioMode = on; 437 // TODO: Need to set the preference for SystemAudioMode. 438 mService.announceSystemAudioModeChange(on); 439 } 440 } 441 } 442 443 boolean getSystemAudioMode() { 444 synchronized (mLock) { 445 return mSystemAudioMode; 446 } 447 } 448 449 /** 450 * Change ARC status into the given {@code enabled} status. 451 * 452 * @return {@code true} if ARC was in "Enabled" status 453 */ 454 boolean setArcStatus(boolean enabled) { 455 synchronized (mLock) { 456 boolean oldStatus = mArcStatusEnabled; 457 // 1. Enable/disable ARC circuit. 458 mService.setAudioReturnChannel(enabled); 459 // 2. Notify arc status to audio service. 460 notifyArcStatusToAudioService(enabled); 461 // 3. Update arc status; 462 mArcStatusEnabled = enabled; 463 return oldStatus; 464 } 465 } 466 467 private void notifyArcStatusToAudioService(boolean enabled) { 468 // Note that we don't set any name to ARC. 469 mService.getAudioManager().setWiredDeviceConnectionState( 470 AudioSystem.DEVICE_OUT_HDMI_ARC, 471 enabled ? 1 : 0, ""); 472 } 473 474 /** 475 * Returns whether ARC is enabled or not. 476 */ 477 boolean getArcStatus() { 478 synchronized (mLock) { 479 return mArcStatusEnabled; 480 } 481 } 482 483 void setAudioStatus(boolean mute, int volume) { 484 synchronized (mLock) { 485 mSystemAudioMute = mute; 486 mSystemAudioVolume = volume; 487 // TODO: pass volume to service (audio service) after scale it to local volume level. 488 mService.setAudioStatus(mute, volume); 489 } 490 } 491 492 @ServiceThreadOnly 493 void changeVolume(int curVolume, int delta, int maxVolume) { 494 assertRunOnServiceThread(); 495 if (delta == 0 || !isSystemAudioOn()) { 496 return; 497 } 498 499 int targetVolume = curVolume + delta; 500 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 501 synchronized (mLock) { 502 // If new volume is the same as current system audio volume, just ignore it. 503 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 504 if (cecVolume == mSystemAudioVolume) { 505 // Update tv volume with system volume value. 506 mService.setAudioStatus(false, 507 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 508 return; 509 } 510 } 511 512 // Remove existing volume action. 513 removeAction(VolumeControlAction.class); 514 515 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 516 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 517 cecVolume, delta > 0)); 518 } 519 520 @ServiceThreadOnly 521 void changeMute(boolean mute) { 522 assertRunOnServiceThread(); 523 if (!isSystemAudioOn()) { 524 return; 525 } 526 527 // Remove existing volume action. 528 removeAction(VolumeControlAction.class); 529 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 530 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 531 } 532 533 private boolean isSystemAudioOn() { 534 if (getAvrDeviceInfo() == null) { 535 return false; 536 } 537 538 synchronized (mLock) { 539 return mSystemAudioMode; 540 } 541 } 542 543 @Override 544 @ServiceThreadOnly 545 protected boolean handleInitiateArc(HdmiCecMessage message) { 546 assertRunOnServiceThread(); 547 // In case where <Initiate Arc> is started by <Request ARC Initiation> 548 // need to clean up RequestArcInitiationAction. 549 removeAction(RequestArcInitiationAction.class); 550 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 551 message.getSource(), true); 552 addAndStartAction(action); 553 return true; 554 } 555 556 @Override 557 @ServiceThreadOnly 558 protected boolean handleTerminateArc(HdmiCecMessage message) { 559 assertRunOnServiceThread(); 560 // In case where <Terminate Arc> is started by <Request ARC Termination> 561 // need to clean up RequestArcInitiationAction. 562 // TODO: check conditions of power status by calling is_connected api 563 // to be added soon. 564 removeAction(RequestArcTerminationAction.class); 565 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 566 message.getSource(), false); 567 addAndStartAction(action); 568 return true; 569 } 570 571 @Override 572 @ServiceThreadOnly 573 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 574 assertRunOnServiceThread(); 575 if (!isMessageForSystemAudio(message)) { 576 return false; 577 } 578 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 579 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 580 addAndStartAction(action); 581 return true; 582 } 583 584 @Override 585 @ServiceThreadOnly 586 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 587 assertRunOnServiceThread(); 588 if (!isMessageForSystemAudio(message)) { 589 return false; 590 } 591 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 592 return true; 593 } 594 595 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 596 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 597 || message.getDestination() != HdmiCec.ADDR_TV 598 || getAvrDeviceInfo() == null) { 599 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 600 return false; 601 } 602 return true; 603 } 604 605 /** 606 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 607 * logical address as new device info's. 608 * 609 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 610 * 611 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 612 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 613 * that has the same logical address as new one has. 614 */ 615 @ServiceThreadOnly 616 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 617 assertRunOnServiceThread(); 618 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 619 if (oldDeviceInfo != null) { 620 removeDeviceInfo(deviceInfo.getLogicalAddress()); 621 } 622 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 623 updateSafeDeviceInfoList(); 624 return oldDeviceInfo; 625 } 626 627 /** 628 * Remove a device info corresponding to the given {@code logicalAddress}. 629 * It returns removed {@link HdmiCecDeviceInfo} if exists. 630 * 631 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 632 * 633 * @param logicalAddress logical address of device to be removed 634 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 635 */ 636 @ServiceThreadOnly 637 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 638 assertRunOnServiceThread(); 639 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 640 if (deviceInfo != null) { 641 mDeviceInfos.remove(logicalAddress); 642 } 643 updateSafeDeviceInfoList(); 644 return deviceInfo; 645 } 646 647 /** 648 * Return a list of all {@link HdmiCecDeviceInfo}. 649 * 650 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 651 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 652 */ 653 @ServiceThreadOnly 654 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 655 assertRunOnServiceThread(); 656 if (includelLocalDevice) { 657 return HdmiUtils.sparseArrayToList(mDeviceInfos); 658 } else { 659 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 660 for (int i = 0; i < mDeviceInfos.size(); ++i) { 661 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 662 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 663 infoList.add(info); 664 } 665 } 666 return infoList; 667 } 668 } 669 670 /** 671 * Return a list of {@link HdmiCecDeviceInfo}. 672 * 673 * @param includeLocalDevice whether to include local device in result. 674 */ 675 List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) { 676 synchronized (mLock) { 677 if (includeLocalDevice) { 678 return mSafeAllDeviceInfos; 679 } else { 680 return mSafeExternalDeviceInfos; 681 } 682 } 683 } 684 685 @ServiceThreadOnly 686 private void updateSafeDeviceInfoList() { 687 assertRunOnServiceThread(); 688 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 689 List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false); 690 synchronized (mLock) { 691 mSafeAllDeviceInfos = copiedDevices; 692 mSafeExternalDeviceInfos = externalDeviceInfos; 693 } 694 } 695 696 @ServiceThreadOnly 697 private boolean isLocalDeviceAddress(int address) { 698 assertRunOnServiceThread(); 699 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 700 if (device.isAddressOf(address)) { 701 return true; 702 } 703 } 704 return false; 705 } 706 707 @ServiceThreadOnly 708 HdmiCecDeviceInfo getAvrDeviceInfo() { 709 assertRunOnServiceThread(); 710 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 711 } 712 713 /** 714 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 715 * 716 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 717 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 718 * 719 * @param logicalAddress logical address to be retrieved 720 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 721 * Returns null if no logical address matched 722 */ 723 @ServiceThreadOnly 724 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 725 assertRunOnServiceThread(); 726 return mDeviceInfos.get(logicalAddress); 727 } 728 729 boolean hasSystemAudioDevice() { 730 return getSafeAvrDeviceInfo() != null; 731 } 732 733 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 734 return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 735 } 736 737 /** 738 * Thread safe version of {@link #getDeviceInfo(int)}. 739 * 740 * @param logicalAddress logical address to be retrieved 741 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 742 * Returns null if no logical address matched 743 */ 744 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 745 synchronized (mLock) { 746 return mSafeAllDeviceInfos.get(logicalAddress); 747 } 748 } 749 750 /** 751 * Called when a device is newly added or a new device is detected. 752 * 753 * @param info device info of a new device. 754 */ 755 @ServiceThreadOnly 756 final void addCecDevice(HdmiCecDeviceInfo info) { 757 assertRunOnServiceThread(); 758 addDeviceInfo(info); 759 if (info.getLogicalAddress() == mAddress) { 760 // The addition of TV device itself should not be notified. 761 return; 762 } 763 mService.invokeDeviceEventListeners(info, true); 764 } 765 766 /** 767 * Called when a device is removed or removal of device is detected. 768 * 769 * @param address a logical address of a device to be removed 770 */ 771 @ServiceThreadOnly 772 final void removeCecDevice(int address) { 773 assertRunOnServiceThread(); 774 HdmiCecDeviceInfo info = removeDeviceInfo(address); 775 mCecMessageCache.flushMessagesFrom(address); 776 mService.invokeDeviceEventListeners(info, false); 777 } 778 779 /** 780 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 781 * the given routing path. CEC devices use routing path for its physical address to 782 * describe the hierarchy of the devices in the network. 783 * 784 * @param path routing path or physical address 785 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 786 */ 787 @ServiceThreadOnly 788 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 789 assertRunOnServiceThread(); 790 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 791 if (info.getPhysicalAddress() == path) { 792 return info; 793 } 794 } 795 return null; 796 } 797 798 /** 799 * Whether a device of the specified physical address and logical address exists 800 * in a device info list. However, both are minimal condition and it could 801 * be different device from the original one. 802 * 803 * @param physicalAddress physical address of a device to be searched 804 * @param logicalAddress logical address of a device to be searched 805 * @return true if exist; otherwise false 806 */ 807 @ServiceThreadOnly 808 boolean isInDeviceList(int physicalAddress, int logicalAddress) { 809 assertRunOnServiceThread(); 810 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 811 if (device == null) { 812 return false; 813 } 814 return device.getPhysicalAddress() == physicalAddress; 815 } 816 817 @Override 818 @ServiceThreadOnly 819 void onHotplug(int portNo, boolean connected) { 820 assertRunOnServiceThread(); 821 822 // Tv device will have permanent HotplugDetectionAction. 823 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 824 if (!hotplugActions.isEmpty()) { 825 // Note that hotplug action is single action running on a machine. 826 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 827 hotplugActions.get(0).pollAllDevicesNow(); 828 } 829 } 830} 831