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