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