HdmiCecLocalDeviceTv.java revision 5e3916a6f14545e033ca1dc56d33ba2983c7ee03
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 if (isConnectedToArcPort(avrInfo.getPhysicalAddress())) { 407 addAndStartAction(new RequestArcInitiationAction( 408 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 409 } 410 } 411 } 412 }); 413 addAndStartAction(action); 414 } 415 416 // Clear all device info. 417 @ServiceThreadOnly 418 private void clearDeviceInfoList() { 419 assertRunOnServiceThread(); 420 mDeviceInfos.clear(); 421 updateSafeDeviceInfoList(); 422 } 423 424 @ServiceThreadOnly 425 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 426 assertRunOnServiceThread(); 427 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 428 if (avr == null) { 429 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 430 return; 431 } 432 433 addAndStartAction( 434 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 435 } 436 437 void setSystemAudioMode(boolean on) { 438 synchronized (mLock) { 439 if (on != mSystemAudioMode) { 440 mSystemAudioMode = on; 441 // TODO: Need to set the preference for SystemAudioMode. 442 mService.announceSystemAudioModeChange(on); 443 } 444 } 445 } 446 447 boolean getSystemAudioMode() { 448 synchronized (mLock) { 449 return mSystemAudioMode; 450 } 451 } 452 453 /** 454 * Change ARC status into the given {@code enabled} status. 455 * 456 * @return {@code true} if ARC was in "Enabled" status 457 */ 458 boolean setArcStatus(boolean enabled) { 459 synchronized (mLock) { 460 boolean oldStatus = mArcStatusEnabled; 461 // 1. Enable/disable ARC circuit. 462 mService.setAudioReturnChannel(enabled); 463 // 2. Notify arc status to audio service. 464 notifyArcStatusToAudioService(enabled); 465 // 3. Update arc status; 466 mArcStatusEnabled = enabled; 467 return oldStatus; 468 } 469 } 470 471 private void notifyArcStatusToAudioService(boolean enabled) { 472 // Note that we don't set any name to ARC. 473 mService.getAudioManager().setWiredDeviceConnectionState( 474 AudioSystem.DEVICE_OUT_HDMI_ARC, 475 enabled ? 1 : 0, ""); 476 } 477 478 /** 479 * Returns whether ARC is enabled or not. 480 */ 481 boolean getArcStatus() { 482 synchronized (mLock) { 483 return mArcStatusEnabled; 484 } 485 } 486 487 void setAudioStatus(boolean mute, int volume) { 488 synchronized (mLock) { 489 mSystemAudioMute = mute; 490 mSystemAudioVolume = volume; 491 // TODO: pass volume to service (audio service) after scale it to local volume level. 492 mService.setAudioStatus(mute, volume); 493 } 494 } 495 496 @ServiceThreadOnly 497 void changeVolume(int curVolume, int delta, int maxVolume) { 498 assertRunOnServiceThread(); 499 if (delta == 0 || !isSystemAudioOn()) { 500 return; 501 } 502 503 int targetVolume = curVolume + delta; 504 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 505 synchronized (mLock) { 506 // If new volume is the same as current system audio volume, just ignore it. 507 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 508 if (cecVolume == mSystemAudioVolume) { 509 // Update tv volume with system volume value. 510 mService.setAudioStatus(false, 511 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 512 return; 513 } 514 } 515 516 // Remove existing volume action. 517 removeAction(VolumeControlAction.class); 518 519 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 520 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 521 cecVolume, delta > 0)); 522 } 523 524 @ServiceThreadOnly 525 void changeMute(boolean mute) { 526 assertRunOnServiceThread(); 527 if (!isSystemAudioOn()) { 528 return; 529 } 530 531 // Remove existing volume action. 532 removeAction(VolumeControlAction.class); 533 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 534 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 535 } 536 537 private boolean isSystemAudioOn() { 538 if (getAvrDeviceInfo() == null) { 539 return false; 540 } 541 542 synchronized (mLock) { 543 return mSystemAudioMode; 544 } 545 } 546 547 @Override 548 @ServiceThreadOnly 549 protected boolean handleInitiateArc(HdmiCecMessage message) { 550 assertRunOnServiceThread(); 551 // In case where <Initiate Arc> is started by <Request ARC Initiation> 552 // need to clean up RequestArcInitiationAction. 553 removeAction(RequestArcInitiationAction.class); 554 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 555 message.getSource(), true); 556 addAndStartAction(action); 557 return true; 558 } 559 560 @Override 561 @ServiceThreadOnly 562 protected boolean handleTerminateArc(HdmiCecMessage message) { 563 assertRunOnServiceThread(); 564 // In case where <Terminate Arc> is started by <Request ARC Termination> 565 // need to clean up RequestArcInitiationAction. 566 // TODO: check conditions of power status by calling is_connected api 567 // to be added soon. 568 removeAction(RequestArcTerminationAction.class); 569 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 570 message.getSource(), false); 571 addAndStartAction(action); 572 return true; 573 } 574 575 @Override 576 @ServiceThreadOnly 577 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 578 assertRunOnServiceThread(); 579 if (!isMessageForSystemAudio(message)) { 580 return false; 581 } 582 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 583 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 584 addAndStartAction(action); 585 return true; 586 } 587 588 @Override 589 @ServiceThreadOnly 590 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 591 assertRunOnServiceThread(); 592 if (!isMessageForSystemAudio(message)) { 593 return false; 594 } 595 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 596 return true; 597 } 598 599 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 600 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 601 || message.getDestination() != HdmiCec.ADDR_TV 602 || getAvrDeviceInfo() == null) { 603 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 604 return false; 605 } 606 return true; 607 } 608 609 /** 610 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 611 * logical address as new device info's. 612 * 613 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 614 * 615 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 616 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 617 * that has the same logical address as new one has. 618 */ 619 @ServiceThreadOnly 620 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 621 assertRunOnServiceThread(); 622 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 623 if (oldDeviceInfo != null) { 624 removeDeviceInfo(deviceInfo.getLogicalAddress()); 625 } 626 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 627 updateSafeDeviceInfoList(); 628 return oldDeviceInfo; 629 } 630 631 /** 632 * Remove a device info corresponding to the given {@code logicalAddress}. 633 * It returns removed {@link HdmiCecDeviceInfo} if exists. 634 * 635 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 636 * 637 * @param logicalAddress logical address of device to be removed 638 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 639 */ 640 @ServiceThreadOnly 641 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 642 assertRunOnServiceThread(); 643 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 644 if (deviceInfo != null) { 645 mDeviceInfos.remove(logicalAddress); 646 } 647 updateSafeDeviceInfoList(); 648 return deviceInfo; 649 } 650 651 /** 652 * Return a list of all {@link HdmiCecDeviceInfo}. 653 * 654 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 655 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 656 */ 657 @ServiceThreadOnly 658 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 659 assertRunOnServiceThread(); 660 if (includelLocalDevice) { 661 return HdmiUtils.sparseArrayToList(mDeviceInfos); 662 } else { 663 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 664 for (int i = 0; i < mDeviceInfos.size(); ++i) { 665 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 666 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 667 infoList.add(info); 668 } 669 } 670 return infoList; 671 } 672 } 673 674 /** 675 * Return a list of {@link HdmiCecDeviceInfo}. 676 * 677 * @param includeLocalDevice whether to include local device in result. 678 */ 679 List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) { 680 synchronized (mLock) { 681 if (includeLocalDevice) { 682 return mSafeAllDeviceInfos; 683 } else { 684 return mSafeExternalDeviceInfos; 685 } 686 } 687 } 688 689 @ServiceThreadOnly 690 private void updateSafeDeviceInfoList() { 691 assertRunOnServiceThread(); 692 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 693 List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false); 694 synchronized (mLock) { 695 mSafeAllDeviceInfos = copiedDevices; 696 mSafeExternalDeviceInfos = externalDeviceInfos; 697 } 698 } 699 700 @ServiceThreadOnly 701 private boolean isLocalDeviceAddress(int address) { 702 assertRunOnServiceThread(); 703 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 704 if (device.isAddressOf(address)) { 705 return true; 706 } 707 } 708 return false; 709 } 710 711 @ServiceThreadOnly 712 HdmiCecDeviceInfo getAvrDeviceInfo() { 713 assertRunOnServiceThread(); 714 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 715 } 716 717 /** 718 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 719 * 720 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 721 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 722 * 723 * @param logicalAddress logical address to be retrieved 724 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 725 * Returns null if no logical address matched 726 */ 727 @ServiceThreadOnly 728 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 729 assertRunOnServiceThread(); 730 return mDeviceInfos.get(logicalAddress); 731 } 732 733 boolean hasSystemAudioDevice() { 734 return getSafeAvrDeviceInfo() != null; 735 } 736 737 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 738 return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 739 } 740 741 /** 742 * Thread safe version of {@link #getDeviceInfo(int)}. 743 * 744 * @param logicalAddress logical address to be retrieved 745 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 746 * Returns null if no logical address matched 747 */ 748 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 749 synchronized (mLock) { 750 return mSafeAllDeviceInfos.get(logicalAddress); 751 } 752 } 753 754 /** 755 * Called when a device is newly added or a new device is detected. 756 * 757 * @param info device info of a new device. 758 */ 759 @ServiceThreadOnly 760 final void addCecDevice(HdmiCecDeviceInfo info) { 761 assertRunOnServiceThread(); 762 addDeviceInfo(info); 763 if (info.getLogicalAddress() == mAddress) { 764 // The addition of TV device itself should not be notified. 765 return; 766 } 767 mService.invokeDeviceEventListeners(info, true); 768 } 769 770 /** 771 * Called when a device is removed or removal of device is detected. 772 * 773 * @param address a logical address of a device to be removed 774 */ 775 @ServiceThreadOnly 776 final void removeCecDevice(int address) { 777 assertRunOnServiceThread(); 778 HdmiCecDeviceInfo info = removeDeviceInfo(address); 779 mCecMessageCache.flushMessagesFrom(address); 780 mService.invokeDeviceEventListeners(info, false); 781 } 782 783 /** 784 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 785 * the given routing path. CEC devices use routing path for its physical address to 786 * describe the hierarchy of the devices in the network. 787 * 788 * @param path routing path or physical address 789 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 790 */ 791 @ServiceThreadOnly 792 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 793 assertRunOnServiceThread(); 794 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 795 if (info.getPhysicalAddress() == path) { 796 return info; 797 } 798 } 799 return null; 800 } 801 802 /** 803 * Whether a device of the specified physical address and logical address exists 804 * in a device info list. However, both are minimal condition and it could 805 * be different device from the original one. 806 * 807 * @param physicalAddress physical address of a device to be searched 808 * @param logicalAddress logical address of a device to be searched 809 * @return true if exist; otherwise false 810 */ 811 @ServiceThreadOnly 812 boolean isInDeviceList(int physicalAddress, int logicalAddress) { 813 assertRunOnServiceThread(); 814 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 815 if (device == null) { 816 return false; 817 } 818 return device.getPhysicalAddress() == physicalAddress; 819 } 820 821 @Override 822 @ServiceThreadOnly 823 void onHotplug(int portNo, boolean connected) { 824 assertRunOnServiceThread(); 825 826 // Tv device will have permanent HotplugDetectionAction. 827 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 828 if (!hotplugActions.isEmpty()) { 829 // Note that hotplug action is single action running on a machine. 830 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 831 hotplugActions.get(0).pollAllDevicesNow(); 832 } 833 } 834} 835