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