HdmiCecLocalDeviceTv.java revision 92b77cf9cbf512e7141cad6fef5a38d0682dde43
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 (mService.isControlEnabled() && 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 (!mService.isControlEnabled() || 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 address = message.getSource(); 247 int path = HdmiUtils.twoBytesToInt(message.getParams()); 248 if (getDeviceInfo(address) == null) { 249 handleNewDeviceAtTheTailOfActivePath(address, path); 250 } else { 251 ActiveSourceHandler.create(this, null).process(address, path); 252 } 253 return true; 254 } 255 256 @Override 257 @ServiceThreadOnly 258 protected boolean handleInactiveSource(HdmiCecMessage message) { 259 assertRunOnServiceThread(); 260 // Seq #10 261 262 // Ignore <Inactive Source> from non-active source device. 263 if (getActiveSource() != message.getSource()) { 264 return true; 265 } 266 if (isInPresetInstallationMode()) { 267 return true; 268 } 269 int portId = getPrevPortId(); 270 if (portId != HdmiConstants.INVALID_PORT_ID) { 271 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 272 273 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 274 if (inactiveSource == null) { 275 return true; 276 } 277 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 278 return true; 279 } 280 // TODO: Switch the TV freeze mode off 281 282 setActivePortId(portId); 283 doManualPortSwitching(portId, null); 284 setPrevPortId(HdmiConstants.INVALID_PORT_ID); 285 } 286 return true; 287 } 288 289 @Override 290 @ServiceThreadOnly 291 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 292 assertRunOnServiceThread(); 293 // Seq #19 294 if (mAddress == getActiveSource()) { 295 mService.sendCecCommand( 296 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 297 } 298 return true; 299 } 300 301 @Override 302 @ServiceThreadOnly 303 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 304 assertRunOnServiceThread(); 305 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 306 mAddress, Locale.getDefault().getISO3Language()); 307 // TODO: figure out how to handle failed to get language code. 308 if (command != null) { 309 mService.sendCecCommand(command); 310 } else { 311 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 312 } 313 return true; 314 } 315 316 @Override 317 @ServiceThreadOnly 318 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 319 assertRunOnServiceThread(); 320 // Ignore if [Device Discovery Action] is going on. 321 if (hasAction(DeviceDiscoveryAction.class)) { 322 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 323 + "because Device Discovery Action is on-going:" + message); 324 return true; 325 } 326 327 int path = HdmiUtils.twoBytesToInt(message.getParams()); 328 int address = message.getSource(); 329 if (!isInDeviceList(path, address)) { 330 handleNewDeviceAtTheTailOfActivePath(address, path); 331 } 332 addAndStartAction(new NewDeviceAction(this, address, path)); 333 return true; 334 } 335 336 private void handleNewDeviceAtTheTailOfActivePath(int address, int path) { 337 // Seq #22 338 if (isTailOfActivePath(path, getActivePath())) { 339 removeAction(RoutingControlAction.class); 340 int newPath = mService.portIdToPath(getActivePortId()); 341 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 342 mAddress, getActivePath(), newPath)); 343 addAndStartAction(new RoutingControlAction(this, getActivePortId(), null)); 344 } 345 } 346 347 /** 348 * Whether the given path is located in the tail of current active path. 349 * 350 * @param path to be tested 351 * @param activePath current active path 352 * @return true if the given path is located in the tail of current active path; otherwise, 353 * false 354 */ 355 static boolean isTailOfActivePath(int path, int activePath) { 356 // If active routing path is internal source, return false. 357 if (activePath == 0) { 358 return false; 359 } 360 for (int i = 12; i >= 0; i -= 4) { 361 int curActivePath = (activePath >> i) & 0xF; 362 if (curActivePath == 0) { 363 return true; 364 } else { 365 int curPath = (path >> i) & 0xF; 366 if (curPath != curActivePath) { 367 return false; 368 } 369 } 370 } 371 return false; 372 } 373 374 @Override 375 @ServiceThreadOnly 376 protected boolean handleRoutingChange(HdmiCecMessage message) { 377 assertRunOnServiceThread(); 378 // Seq #21 379 byte[] params = message.getParams(); 380 if (params.length != 4) { 381 Slog.w(TAG, "Wrong parameter: " + message); 382 return true; 383 } 384 int currentPath = HdmiUtils.twoBytesToInt(params); 385 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 386 int newPath = HdmiUtils.twoBytesToInt(params, 2); 387 setActivePath(newPath); 388 removeAction(RoutingControlAction.class); 389 addAndStartAction(new RoutingControlAction(this, newPath, null)); 390 } 391 return true; 392 } 393 394 @Override 395 @ServiceThreadOnly 396 protected boolean handleVendorSpecificCommand(HdmiCecMessage message) { 397 assertRunOnServiceThread(); 398 List<VendorSpecificAction> actions = Collections.emptyList(); 399 // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions. 400 401 // We assume that there can be multiple vendor-specific command actions running 402 // at the same time. Pass the message to each action to see if one of them needs it. 403 for (VendorSpecificAction action : actions) { 404 if (action.processCommand(message)) { 405 return true; 406 } 407 } 408 // Handle the message here if it is not already consumed by one of the running actions. 409 // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another 410 // vendor-specific action: 411 // 412 // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress)); 413 // 414 // For now, simply reply with <Feature Abort> and mark it consumed by returning true. 415 mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand( 416 message.getDestination(), message.getSource(), message.getOpcode(), 417 HdmiConstants.ABORT_REFUSED)); 418 return true; 419 } 420 421 @Override 422 @ServiceThreadOnly 423 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 424 assertRunOnServiceThread(); 425 426 byte params[] = message.getParams(); 427 if (params.length < 1) { 428 Slog.w(TAG, "Invalide <Report Audio Status> message:" + message); 429 return true; 430 } 431 int mute = params[0] & 0x80; 432 int volume = params[0] & 0x7F; 433 setAudioStatus(mute == 0x80, volume); 434 return true; 435 } 436 437 @ServiceThreadOnly 438 private void launchDeviceDiscovery() { 439 assertRunOnServiceThread(); 440 clearDeviceInfoList(); 441 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 442 new DeviceDiscoveryCallback() { 443 @Override 444 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 445 for (HdmiCecDeviceInfo info : deviceInfos) { 446 addCecDevice(info); 447 } 448 449 // Since we removed all devices when it's start and 450 // device discovery action does not poll local devices, 451 // we should put device info of local device manually here 452 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 453 addCecDevice(device.getDeviceInfo()); 454 } 455 456 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 457 458 // If there is AVR, initiate System Audio Auto initiation action, 459 // which turns on and off system audio according to last system 460 // audio setting. 461 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 462 if (avrInfo != null) { 463 addAndStartAction(new SystemAudioAutoInitiationAction( 464 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 465 if (isConnectedToArcPort(avrInfo.getPhysicalAddress())) { 466 addAndStartAction(new RequestArcInitiationAction( 467 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 468 } 469 } 470 } 471 }); 472 addAndStartAction(action); 473 } 474 475 // Clear all device info. 476 @ServiceThreadOnly 477 private void clearDeviceInfoList() { 478 assertRunOnServiceThread(); 479 mDeviceInfos.clear(); 480 updateSafeDeviceInfoList(); 481 } 482 483 @ServiceThreadOnly 484 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 485 assertRunOnServiceThread(); 486 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 487 if (avr == null) { 488 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 489 return; 490 } 491 492 addAndStartAction( 493 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 494 } 495 496 void setSystemAudioMode(boolean on) { 497 synchronized (mLock) { 498 if (on != mSystemAudioMode) { 499 mSystemAudioMode = on; 500 // TODO: Need to set the preference for SystemAudioMode. 501 mService.announceSystemAudioModeChange(on); 502 } 503 } 504 } 505 506 boolean getSystemAudioMode() { 507 synchronized (mLock) { 508 return mSystemAudioMode; 509 } 510 } 511 512 /** 513 * Change ARC status into the given {@code enabled} status. 514 * 515 * @return {@code true} if ARC was in "Enabled" status 516 */ 517 boolean setArcStatus(boolean enabled) { 518 synchronized (mLock) { 519 boolean oldStatus = mArcStatusEnabled; 520 // 1. Enable/disable ARC circuit. 521 mService.setAudioReturnChannel(enabled); 522 // 2. Notify arc status to audio service. 523 notifyArcStatusToAudioService(enabled); 524 // 3. Update arc status; 525 mArcStatusEnabled = enabled; 526 return oldStatus; 527 } 528 } 529 530 private void notifyArcStatusToAudioService(boolean enabled) { 531 // Note that we don't set any name to ARC. 532 mService.getAudioManager().setWiredDeviceConnectionState( 533 AudioSystem.DEVICE_OUT_HDMI_ARC, 534 enabled ? 1 : 0, ""); 535 } 536 537 /** 538 * Returns whether ARC is enabled or not. 539 */ 540 boolean getArcStatus() { 541 synchronized (mLock) { 542 return mArcStatusEnabled; 543 } 544 } 545 546 void setAudioStatus(boolean mute, int volume) { 547 synchronized (mLock) { 548 mSystemAudioMute = mute; 549 mSystemAudioVolume = volume; 550 // TODO: pass volume to service (audio service) after scale it to local volume level. 551 mService.setAudioStatus(mute, volume); 552 } 553 } 554 555 @ServiceThreadOnly 556 void changeVolume(int curVolume, int delta, int maxVolume) { 557 assertRunOnServiceThread(); 558 if (delta == 0 || !isSystemAudioOn()) { 559 return; 560 } 561 562 int targetVolume = curVolume + delta; 563 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 564 synchronized (mLock) { 565 // If new volume is the same as current system audio volume, just ignore it. 566 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 567 if (cecVolume == mSystemAudioVolume) { 568 // Update tv volume with system volume value. 569 mService.setAudioStatus(false, 570 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 571 return; 572 } 573 } 574 575 // Remove existing volume action. 576 removeAction(VolumeControlAction.class); 577 578 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 579 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 580 cecVolume, delta > 0)); 581 } 582 583 @ServiceThreadOnly 584 void changeMute(boolean mute) { 585 assertRunOnServiceThread(); 586 if (!isSystemAudioOn()) { 587 return; 588 } 589 590 // Remove existing volume action. 591 removeAction(VolumeControlAction.class); 592 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 593 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 594 } 595 596 private boolean isSystemAudioOn() { 597 if (getAvrDeviceInfo() == null) { 598 return false; 599 } 600 601 synchronized (mLock) { 602 return mSystemAudioMode; 603 } 604 } 605 606 @Override 607 @ServiceThreadOnly 608 protected boolean handleInitiateArc(HdmiCecMessage message) { 609 assertRunOnServiceThread(); 610 // In case where <Initiate Arc> is started by <Request ARC Initiation> 611 // need to clean up RequestArcInitiationAction. 612 removeAction(RequestArcInitiationAction.class); 613 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 614 message.getSource(), true); 615 addAndStartAction(action); 616 return true; 617 } 618 619 @Override 620 @ServiceThreadOnly 621 protected boolean handleTerminateArc(HdmiCecMessage message) { 622 assertRunOnServiceThread(); 623 // In case where <Terminate Arc> is started by <Request ARC Termination> 624 // need to clean up RequestArcInitiationAction. 625 // TODO: check conditions of power status by calling is_connected api 626 // to be added soon. 627 removeAction(RequestArcTerminationAction.class); 628 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 629 message.getSource(), false); 630 addAndStartAction(action); 631 return true; 632 } 633 634 @Override 635 @ServiceThreadOnly 636 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 637 assertRunOnServiceThread(); 638 if (!isMessageForSystemAudio(message)) { 639 return false; 640 } 641 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 642 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 643 addAndStartAction(action); 644 return true; 645 } 646 647 @Override 648 @ServiceThreadOnly 649 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 650 assertRunOnServiceThread(); 651 if (!isMessageForSystemAudio(message)) { 652 return false; 653 } 654 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 655 return true; 656 } 657 658 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 659 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 660 || message.getDestination() != HdmiCec.ADDR_TV 661 || getAvrDeviceInfo() == null) { 662 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 663 return false; 664 } 665 return true; 666 } 667 668 /** 669 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 670 * logical address as new device info's. 671 * 672 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 673 * 674 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 675 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 676 * that has the same logical address as new one has. 677 */ 678 @ServiceThreadOnly 679 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 680 assertRunOnServiceThread(); 681 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 682 if (oldDeviceInfo != null) { 683 removeDeviceInfo(deviceInfo.getLogicalAddress()); 684 } 685 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 686 updateSafeDeviceInfoList(); 687 return oldDeviceInfo; 688 } 689 690 /** 691 * Remove a device info corresponding to the given {@code logicalAddress}. 692 * It returns removed {@link HdmiCecDeviceInfo} if exists. 693 * 694 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 695 * 696 * @param logicalAddress logical address of device to be removed 697 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 698 */ 699 @ServiceThreadOnly 700 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 701 assertRunOnServiceThread(); 702 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 703 if (deviceInfo != null) { 704 mDeviceInfos.remove(logicalAddress); 705 } 706 updateSafeDeviceInfoList(); 707 return deviceInfo; 708 } 709 710 /** 711 * Return a list of all {@link HdmiCecDeviceInfo}. 712 * 713 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 714 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 715 */ 716 @ServiceThreadOnly 717 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 718 assertRunOnServiceThread(); 719 if (includelLocalDevice) { 720 return HdmiUtils.sparseArrayToList(mDeviceInfos); 721 } else { 722 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 723 for (int i = 0; i < mDeviceInfos.size(); ++i) { 724 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 725 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 726 infoList.add(info); 727 } 728 } 729 return infoList; 730 } 731 } 732 733 /** 734 * Return a list of {@link HdmiCecDeviceInfo}. 735 * 736 * @param includeLocalDevice whether to include local device in result. 737 */ 738 List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) { 739 synchronized (mLock) { 740 if (includeLocalDevice) { 741 return mSafeAllDeviceInfos; 742 } else { 743 return mSafeExternalDeviceInfos; 744 } 745 } 746 } 747 748 @ServiceThreadOnly 749 private void updateSafeDeviceInfoList() { 750 assertRunOnServiceThread(); 751 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 752 List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false); 753 synchronized (mLock) { 754 mSafeAllDeviceInfos = copiedDevices; 755 mSafeExternalDeviceInfos = externalDeviceInfos; 756 } 757 } 758 759 @ServiceThreadOnly 760 private boolean isLocalDeviceAddress(int address) { 761 assertRunOnServiceThread(); 762 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 763 if (device.isAddressOf(address)) { 764 return true; 765 } 766 } 767 return false; 768 } 769 770 @ServiceThreadOnly 771 HdmiCecDeviceInfo getAvrDeviceInfo() { 772 assertRunOnServiceThread(); 773 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 774 } 775 776 /** 777 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 778 * 779 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 780 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 781 * 782 * @param logicalAddress logical address to be retrieved 783 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 784 * Returns null if no logical address matched 785 */ 786 @ServiceThreadOnly 787 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 788 assertRunOnServiceThread(); 789 return mDeviceInfos.get(logicalAddress); 790 } 791 792 boolean hasSystemAudioDevice() { 793 return getSafeAvrDeviceInfo() != null; 794 } 795 796 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 797 return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 798 } 799 800 /** 801 * Thread safe version of {@link #getDeviceInfo(int)}. 802 * 803 * @param logicalAddress logical address to be retrieved 804 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 805 * Returns null if no logical address matched 806 */ 807 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 808 synchronized (mLock) { 809 return mSafeAllDeviceInfos.get(logicalAddress); 810 } 811 } 812 813 /** 814 * Called when a device is newly added or a new device is detected. 815 * 816 * @param info device info of a new device. 817 */ 818 @ServiceThreadOnly 819 final void addCecDevice(HdmiCecDeviceInfo info) { 820 assertRunOnServiceThread(); 821 addDeviceInfo(info); 822 if (info.getLogicalAddress() == mAddress) { 823 // The addition of TV device itself should not be notified. 824 return; 825 } 826 mService.invokeDeviceEventListeners(info, true); 827 } 828 829 /** 830 * Called when a device is removed or removal of device is detected. 831 * 832 * @param address a logical address of a device to be removed 833 */ 834 @ServiceThreadOnly 835 final void removeCecDevice(int address) { 836 assertRunOnServiceThread(); 837 HdmiCecDeviceInfo info = removeDeviceInfo(address); 838 handleRemoveActiveRoutingPath(info.getPhysicalAddress()); 839 mCecMessageCache.flushMessagesFrom(address); 840 mService.invokeDeviceEventListeners(info, false); 841 } 842 843 private void handleRemoveActiveRoutingPath(int path) { 844 // Seq #23 845 if (isTailOfActivePath(path, getActivePath())) { 846 removeAction(RoutingControlAction.class); 847 int newPath = mService.portIdToPath(getActivePortId()); 848 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 849 mAddress, getActivePath(), newPath)); 850 addAndStartAction(new RoutingControlAction(this, getActivePortId(), null)); 851 } 852 } 853 854 @ServiceThreadOnly 855 void routingAtEnableTime() { 856 assertRunOnServiceThread(); 857 // Seq #24 858 if (getActivePortId() != HdmiConstants.INVALID_PORT_ID) { 859 // TODO: Check if TV was not powered on due to <Text/Image View On>, 860 // TV is not in Preset Installation mode, not in initial setup mode, not 861 // in Software updating mode, not in service mode, for following actions. 862 removeAction(RoutingControlAction.class); 863 int newPath = mService.portIdToPath(getActivePortId()); 864 mService.sendCecCommand( 865 HdmiCecMessageBuilder.buildRoutingChange(mAddress, getActivePath(), newPath)); 866 addAndStartAction(new RoutingControlAction(this, getActivePortId(), null)); 867 } else { 868 int activePath = mService.getPhysicalAddress(); 869 setActivePath(activePath); 870 // TODO: Do following only when TV was not powered on due to <Text/Image View On>. 871 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, activePath)); 872 } 873 } 874 875 /** 876 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 877 * the given routing path. CEC devices use routing path for its physical address to 878 * describe the hierarchy of the devices in the network. 879 * 880 * @param path routing path or physical address 881 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 882 */ 883 @ServiceThreadOnly 884 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 885 assertRunOnServiceThread(); 886 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 887 if (info.getPhysicalAddress() == path) { 888 return info; 889 } 890 } 891 return null; 892 } 893 894 /** 895 * Whether a device of the specified physical address and logical address exists 896 * in a device info list. However, both are minimal condition and it could 897 * be different device from the original one. 898 * 899 * @param logicalAddress logical address of a device to be searched 900 * @param physicalAddress physical address of a device to be searched 901 * @return true if exist; otherwise false 902 */ 903 @ServiceThreadOnly 904 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 905 assertRunOnServiceThread(); 906 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 907 if (device == null) { 908 return false; 909 } 910 return device.getPhysicalAddress() == physicalAddress; 911 } 912 913 @Override 914 @ServiceThreadOnly 915 void onHotplug(int portId, boolean connected) { 916 assertRunOnServiceThread(); 917 918 // Tv device will have permanent HotplugDetectionAction. 919 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 920 if (!hotplugActions.isEmpty()) { 921 // Note that hotplug action is single action running on a machine. 922 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 923 hotplugActions.get(0).pollAllDevicesNow(); 924 } 925 } 926} 927