HdmiCecLocalDeviceTv.java revision cae6627702a3bae3121f0ebbb9c8069e77f81cc7
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 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 != HdmiConstants.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(HdmiConstants.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 handleVendorSpecificCommand(HdmiCecMessage message) { 409 assertRunOnServiceThread(); 410 List<VendorSpecificAction> actions = Collections.emptyList(); 411 // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions. 412 413 // We assume that there can be multiple vendor-specific command actions running 414 // at the same time. Pass the message to each action to see if one of them needs it. 415 for (VendorSpecificAction action : actions) { 416 if (action.processCommand(message)) { 417 return true; 418 } 419 } 420 // Handle the message here if it is not already consumed by one of the running actions. 421 // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another 422 // vendor-specific action: 423 // 424 // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress)); 425 // 426 // For now, simply reply with <Feature Abort> and mark it consumed by returning true. 427 mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand( 428 message.getDestination(), message.getSource(), message.getOpcode(), 429 HdmiConstants.ABORT_REFUSED)); 430 return true; 431 } 432 433 @Override 434 @ServiceThreadOnly 435 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 436 assertRunOnServiceThread(); 437 438 byte params[] = message.getParams(); 439 if (params.length < 1) { 440 Slog.w(TAG, "Invalide <Report Audio Status> message:" + message); 441 return true; 442 } 443 int mute = params[0] & 0x80; 444 int volume = params[0] & 0x7F; 445 setAudioStatus(mute == 0x80, volume); 446 return true; 447 } 448 449 @Override 450 @ServiceThreadOnly 451 protected boolean handleTextViewOn(HdmiCecMessage message) { 452 assertRunOnServiceThread(); 453 if (mService.isPowerStandbyOrTransient()) { 454 mService.wakeUp(); 455 } 456 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 457 // that represents the source device. 458 return true; 459 } 460 461 @Override 462 @ServiceThreadOnly 463 protected boolean handleImageViewOn(HdmiCecMessage message) { 464 assertRunOnServiceThread(); 465 // Currently, it's the same as <Text View On>. 466 return handleTextViewOn(message); 467 } 468 469 @ServiceThreadOnly 470 private void launchDeviceDiscovery() { 471 assertRunOnServiceThread(); 472 clearDeviceInfoList(); 473 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 474 new DeviceDiscoveryCallback() { 475 @Override 476 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 477 for (HdmiCecDeviceInfo info : deviceInfos) { 478 addCecDevice(info); 479 } 480 481 // Since we removed all devices when it's start and 482 // device discovery action does not poll local devices, 483 // we should put device info of local device manually here 484 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 485 addCecDevice(device.getDeviceInfo()); 486 } 487 488 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 489 490 // If there is AVR, initiate System Audio Auto initiation action, 491 // which turns on and off system audio according to last system 492 // audio setting. 493 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 494 if (avrInfo != null) { 495 addAndStartAction(new SystemAudioAutoInitiationAction( 496 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 497 if (mArcEstablished) { 498 startArcAction(true); 499 } 500 } 501 } 502 }); 503 addAndStartAction(action); 504 } 505 506 // Clear all device info. 507 @ServiceThreadOnly 508 private void clearDeviceInfoList() { 509 assertRunOnServiceThread(); 510 mDeviceInfos.clear(); 511 updateSafeDeviceInfoList(); 512 } 513 514 @ServiceThreadOnly 515 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 516 assertRunOnServiceThread(); 517 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 518 if (avr == null) { 519 invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE); 520 return; 521 } 522 523 addAndStartAction( 524 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 525 } 526 527 // # Seq 25 528 void setSystemAudioMode(boolean on) { 529 synchronized (mLock) { 530 if (on != mSystemAudioMode) { 531 mSystemAudioMode = on; 532 // TODO: Need to set the preference for SystemAudioMode. 533 mService.announceSystemAudioModeChange(on); 534 } 535 } 536 } 537 538 boolean getSystemAudioMode() { 539 synchronized (mLock) { 540 return mSystemAudioMode; 541 } 542 } 543 544 /** 545 * Change ARC status into the given {@code enabled} status. 546 * 547 * @return {@code true} if ARC was in "Enabled" status 548 */ 549 @ServiceThreadOnly 550 boolean setArcStatus(boolean enabled) { 551 assertRunOnServiceThread(); 552 boolean oldStatus = mArcEstablished; 553 // 1. Enable/disable ARC circuit. 554 mService.setAudioReturnChannel(enabled); 555 // 2. Notify arc status to audio service. 556 notifyArcStatusToAudioService(enabled); 557 // 3. Update arc status; 558 mArcEstablished = enabled; 559 return oldStatus; 560 } 561 562 private void notifyArcStatusToAudioService(boolean enabled) { 563 // Note that we don't set any name to ARC. 564 mService.getAudioManager().setWiredDeviceConnectionState( 565 AudioSystem.DEVICE_OUT_HDMI_ARC, 566 enabled ? 1 : 0, ""); 567 } 568 569 /** 570 * Returns whether ARC is enabled or not. 571 */ 572 @ServiceThreadOnly 573 boolean isArcEstabilished() { 574 assertRunOnServiceThread(); 575 return mArcFeatureEnabled && mArcEstablished; 576 } 577 578 @ServiceThreadOnly 579 void changeArcFeatureEnabled(boolean enabled) { 580 assertRunOnServiceThread(); 581 582 if (mArcFeatureEnabled != enabled) { 583 if (enabled) { 584 if (!mArcEstablished) { 585 startArcAction(true); 586 } 587 } else { 588 if (mArcEstablished) { 589 startArcAction(false); 590 } 591 } 592 mArcFeatureEnabled = enabled; 593 } 594 } 595 596 @ServiceThreadOnly 597 boolean isArcFeatureEnabled() { 598 assertRunOnServiceThread(); 599 return mArcFeatureEnabled; 600 } 601 602 @ServiceThreadOnly 603 private void startArcAction(boolean enabled) { 604 assertRunOnServiceThread(); 605 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 606 if (info == null) { 607 return; 608 } 609 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 610 return; 611 } 612 613 // Terminate opposite action and start action if not exist. 614 if (enabled) { 615 removeAction(RequestArcTerminationAction.class); 616 if (!hasAction(RequestArcInitiationAction.class)) { 617 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 618 } 619 } else { 620 removeAction(RequestArcInitiationAction.class); 621 if (!hasAction(RequestArcTerminationAction.class)) { 622 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 623 } 624 } 625 } 626 627 void setAudioStatus(boolean mute, int volume) { 628 synchronized (mLock) { 629 mSystemAudioMute = mute; 630 mSystemAudioVolume = volume; 631 // TODO: pass volume to service (audio service) after scale it to local volume level. 632 mService.setAudioStatus(mute, volume); 633 } 634 } 635 636 @ServiceThreadOnly 637 void changeVolume(int curVolume, int delta, int maxVolume) { 638 assertRunOnServiceThread(); 639 if (delta == 0 || !isSystemAudioOn()) { 640 return; 641 } 642 643 int targetVolume = curVolume + delta; 644 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 645 synchronized (mLock) { 646 // If new volume is the same as current system audio volume, just ignore it. 647 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 648 if (cecVolume == mSystemAudioVolume) { 649 // Update tv volume with system volume value. 650 mService.setAudioStatus(false, 651 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 652 return; 653 } 654 } 655 656 // Remove existing volume action. 657 removeAction(VolumeControlAction.class); 658 659 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 660 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 661 cecVolume, delta > 0)); 662 } 663 664 @ServiceThreadOnly 665 void changeMute(boolean mute) { 666 assertRunOnServiceThread(); 667 if (!isSystemAudioOn()) { 668 return; 669 } 670 671 // Remove existing volume action. 672 removeAction(VolumeControlAction.class); 673 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 674 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 675 } 676 677 private boolean isSystemAudioOn() { 678 if (getAvrDeviceInfo() == null) { 679 return false; 680 } 681 682 synchronized (mLock) { 683 return mSystemAudioMode; 684 } 685 } 686 687 @Override 688 @ServiceThreadOnly 689 protected boolean handleInitiateArc(HdmiCecMessage message) { 690 assertRunOnServiceThread(); 691 // In case where <Initiate Arc> is started by <Request ARC Initiation> 692 // need to clean up RequestArcInitiationAction. 693 removeAction(RequestArcInitiationAction.class); 694 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 695 message.getSource(), true); 696 addAndStartAction(action); 697 return true; 698 } 699 700 @Override 701 @ServiceThreadOnly 702 protected boolean handleTerminateArc(HdmiCecMessage message) { 703 assertRunOnServiceThread(); 704 // In case where <Terminate Arc> is started by <Request ARC Termination> 705 // need to clean up RequestArcInitiationAction. 706 // TODO: check conditions of power status by calling is_connected api 707 // to be added soon. 708 removeAction(RequestArcTerminationAction.class); 709 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 710 message.getSource(), false); 711 addAndStartAction(action); 712 return true; 713 } 714 715 @Override 716 @ServiceThreadOnly 717 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 718 assertRunOnServiceThread(); 719 if (!isMessageForSystemAudio(message)) { 720 return false; 721 } 722 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 723 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 724 addAndStartAction(action); 725 return true; 726 } 727 728 @Override 729 @ServiceThreadOnly 730 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 731 assertRunOnServiceThread(); 732 if (!isMessageForSystemAudio(message)) { 733 return false; 734 } 735 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 736 return true; 737 } 738 739 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 740 if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM 741 || message.getDestination() != HdmiCec.ADDR_TV 742 || getAvrDeviceInfo() == null) { 743 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 744 return false; 745 } 746 return true; 747 } 748 749 /** 750 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 751 * logical address as new device info's. 752 * 753 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 754 * 755 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 756 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 757 * that has the same logical address as new one has. 758 */ 759 @ServiceThreadOnly 760 HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 761 assertRunOnServiceThread(); 762 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 763 if (oldDeviceInfo != null) { 764 removeDeviceInfo(deviceInfo.getLogicalAddress()); 765 } 766 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 767 updateSafeDeviceInfoList(); 768 return oldDeviceInfo; 769 } 770 771 /** 772 * Remove a device info corresponding to the given {@code logicalAddress}. 773 * It returns removed {@link HdmiCecDeviceInfo} if exists. 774 * 775 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 776 * 777 * @param logicalAddress logical address of device to be removed 778 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 779 */ 780 @ServiceThreadOnly 781 HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 782 assertRunOnServiceThread(); 783 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 784 if (deviceInfo != null) { 785 mDeviceInfos.remove(logicalAddress); 786 } 787 updateSafeDeviceInfoList(); 788 return deviceInfo; 789 } 790 791 /** 792 * Return a list of all {@link HdmiCecDeviceInfo}. 793 * 794 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 795 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 796 */ 797 @ServiceThreadOnly 798 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 799 assertRunOnServiceThread(); 800 if (includelLocalDevice) { 801 return HdmiUtils.sparseArrayToList(mDeviceInfos); 802 } else { 803 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 804 for (int i = 0; i < mDeviceInfos.size(); ++i) { 805 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 806 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 807 infoList.add(info); 808 } 809 } 810 return infoList; 811 } 812 } 813 814 /** 815 * Return external input devices. 816 */ 817 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 818 synchronized (mLock) { 819 return mSafeExternalInputs; 820 } 821 } 822 823 @ServiceThreadOnly 824 private void updateSafeDeviceInfoList() { 825 assertRunOnServiceThread(); 826 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 827 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 828 synchronized (mLock) { 829 mSafeAllDeviceInfos = copiedDevices; 830 mSafeExternalInputs = externalInputs; 831 } 832 } 833 834 /** 835 * Return a list of external cec input (source) devices. 836 * 837 * <p>Note that this effectively excludes non-source devices like system audio, 838 * secondary TV. 839 */ 840 private List<HdmiCecDeviceInfo> getInputDevices() { 841 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 842 for (int i = 0; i < mDeviceInfos.size(); ++i) { 843 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 844 if (isLocalDeviceAddress(i)) { 845 continue; 846 } 847 if (info.isSourceType()) { 848 infoList.add(info); 849 } 850 } 851 return infoList; 852 } 853 854 @ServiceThreadOnly 855 private boolean isLocalDeviceAddress(int address) { 856 assertRunOnServiceThread(); 857 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 858 if (device.isAddressOf(address)) { 859 return true; 860 } 861 } 862 return false; 863 } 864 865 @ServiceThreadOnly 866 HdmiCecDeviceInfo getAvrDeviceInfo() { 867 assertRunOnServiceThread(); 868 return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 869 } 870 871 /** 872 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 873 * 874 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 875 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 876 * 877 * @param logicalAddress logical address to be retrieved 878 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 879 * Returns null if no logical address matched 880 */ 881 @ServiceThreadOnly 882 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 883 assertRunOnServiceThread(); 884 return mDeviceInfos.get(logicalAddress); 885 } 886 887 boolean hasSystemAudioDevice() { 888 return getSafeAvrDeviceInfo() != null; 889 } 890 891 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 892 return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM); 893 } 894 895 /** 896 * Thread safe version of {@link #getDeviceInfo(int)}. 897 * 898 * @param logicalAddress logical address to be retrieved 899 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 900 * Returns null if no logical address matched 901 */ 902 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 903 synchronized (mLock) { 904 return mSafeAllDeviceInfos.get(logicalAddress); 905 } 906 } 907 908 /** 909 * Called when a device is newly added or a new device is detected. 910 * 911 * @param info device info of a new device. 912 */ 913 @ServiceThreadOnly 914 final void addCecDevice(HdmiCecDeviceInfo info) { 915 assertRunOnServiceThread(); 916 addDeviceInfo(info); 917 if (info.getLogicalAddress() == mAddress) { 918 // The addition of TV device itself should not be notified. 919 return; 920 } 921 mService.invokeDeviceEventListeners(info, true); 922 } 923 924 /** 925 * Called when a device is removed or removal of device is detected. 926 * 927 * @param address a logical address of a device to be removed 928 */ 929 @ServiceThreadOnly 930 final void removeCecDevice(int address) { 931 assertRunOnServiceThread(); 932 HdmiCecDeviceInfo info = removeDeviceInfo(address); 933 934 mCecMessageCache.flushMessagesFrom(address); 935 mService.invokeDeviceEventListeners(info, false); 936 } 937 938 @ServiceThreadOnly 939 void handleRemoveActiveRoutingPath(int path) { 940 assertRunOnServiceThread(); 941 // Seq #23 942 if (isTailOfActivePath(path, getActivePath())) { 943 removeAction(RoutingControlAction.class); 944 int newPath = mService.portIdToPath(getActivePortId()); 945 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 946 mAddress, getActivePath(), newPath)); 947 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 948 } 949 } 950 951 /** 952 * Launch routing control process. 953 * 954 * @param routingForBootup true if routing control is initiated due to One Touch Play 955 * or TV power on 956 */ 957 @ServiceThreadOnly 958 void launchRoutingControl(boolean routingForBootup) { 959 assertRunOnServiceThread(); 960 // Seq #24 961 if (getActivePortId() != HdmiConstants.INVALID_PORT_ID) { 962 if (!routingForBootup && !isProhibitMode()) { 963 removeAction(RoutingControlAction.class); 964 int newPath = mService.portIdToPath(getActivePortId()); 965 setActivePath(newPath); 966 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 967 getActivePath(), newPath)); 968 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 969 routingForBootup, null)); 970 } 971 } else { 972 int activePath = mService.getPhysicalAddress(); 973 setActivePath(activePath); 974 if (!routingForBootup) { 975 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 976 activePath)); 977 } 978 } 979 } 980 981 /** 982 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 983 * the given routing path. CEC devices use routing path for its physical address to 984 * describe the hierarchy of the devices in the network. 985 * 986 * @param path routing path or physical address 987 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 988 */ 989 @ServiceThreadOnly 990 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 991 assertRunOnServiceThread(); 992 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 993 if (info.getPhysicalAddress() == path) { 994 return info; 995 } 996 } 997 return null; 998 } 999 1000 /** 1001 * Whether a device of the specified physical address and logical address exists 1002 * in a device info list. However, both are minimal condition and it could 1003 * be different device from the original one. 1004 * 1005 * @param logicalAddress logical address of a device to be searched 1006 * @param physicalAddress physical address of a device to be searched 1007 * @return true if exist; otherwise false 1008 */ 1009 @ServiceThreadOnly 1010 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1011 assertRunOnServiceThread(); 1012 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1013 if (device == null) { 1014 return false; 1015 } 1016 return device.getPhysicalAddress() == physicalAddress; 1017 } 1018 1019 @Override 1020 @ServiceThreadOnly 1021 void onHotplug(int portId, boolean connected) { 1022 assertRunOnServiceThread(); 1023 1024 // Tv device will have permanent HotplugDetectionAction. 1025 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1026 if (!hotplugActions.isEmpty()) { 1027 // Note that hotplug action is single action running on a machine. 1028 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1029 hotplugActions.get(0).pollAllDevicesNow(); 1030 } 1031 } 1032 1033 @ServiceThreadOnly 1034 void setAutoDeviceOff(boolean enabled) { 1035 assertRunOnServiceThread(); 1036 mAutoDeviceOff = enabled; 1037 } 1038 1039 @Override 1040 @ServiceThreadOnly 1041 protected void onTransitionToStandby(boolean initiatedByCec) { 1042 assertRunOnServiceThread(); 1043 // Remove any repeated working actions. 1044 // HotplugDetectionAction will be reinstated during the wake up process. 1045 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1046 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1047 removeAction(HotplugDetectionAction.class); 1048 checkIfPendingActionsCleared(); 1049 } 1050 1051 @Override 1052 @ServiceThreadOnly 1053 protected void onStandBy(boolean initiatedByCec) { 1054 assertRunOnServiceThread(); 1055 // Seq #11 1056 if (!mService.isControlEnabled()) { 1057 return; 1058 } 1059 if (!initiatedByCec) { 1060 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1061 mAddress, HdmiCec.ADDR_BROADCAST)); 1062 } 1063 } 1064 1065 @Override 1066 @ServiceThreadOnly 1067 protected boolean handleStandby(HdmiCecMessage message) { 1068 assertRunOnServiceThread(); 1069 // Seq #12 1070 // Tv accepts directly addressed <Standby> only. 1071 if (message.getDestination() == mAddress) { 1072 super.handleStandby(message); 1073 } 1074 return false; 1075 } 1076 1077 boolean isProhibitMode() { 1078 return mService.isProhibitMode(); 1079 } 1080} 1081