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