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