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