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