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