HdmiCecLocalDeviceTv.java revision 7ecfbaed6e902aea151bc1919cf7771bbd868fc4
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.provider.Settings.Global; 30import android.util.Slog; 31import android.util.SparseArray; 32 33import com.android.internal.annotations.GuardedBy; 34import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 35import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 36 37import java.io.UnsupportedEncodingException; 38import java.util.ArrayList; 39import java.util.Collections; 40import java.util.List; 41import java.util.Locale; 42 43/** 44 * Represent a logical device of type TV residing in Android system. 45 */ 46final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 47 private static final String TAG = "HdmiCecLocalDeviceTv"; 48 49 // Whether ARC is available or not. "true" means that ARC is estabilished between TV and 50 // AVR as audio receiver. 51 @ServiceThreadOnly 52 private boolean mArcEstablished = false; 53 54 // Whether ARC feature is enabled or not. 55 private boolean mArcFeatureEnabled = false; 56 57 // Whether SystemAudioMode is "On" or not. 58 @GuardedBy("mLock") 59 private boolean mSystemAudioMode; 60 61 // The previous port id (input) before switching to the new one. This is remembered in order to 62 // be able to switch to it upon receiving <Inactive Source> from currently active source. 63 // This remains valid only when the active source was switched via one touch play operation 64 // (either by TV or source device). Manual port switching invalidates this value to 65 // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything. 66 @GuardedBy("mLock") 67 private int mPrevPortId; 68 69 @GuardedBy("mLock") 70 private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME; 71 72 @GuardedBy("mLock") 73 private boolean mSystemAudioMute = false; 74 75 // Copy of mDeviceInfos to guarantee thread-safety. 76 @GuardedBy("mLock") 77 private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 78 // All external cec input(source) devices. Does not include system audio device. 79 @GuardedBy("mLock") 80 private List<HdmiCecDeviceInfo> mSafeExternalInputs = Collections.emptyList(); 81 82 // Map-like container of all cec devices including local ones. 83 // A logical address of device is used as key of container. 84 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 85 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 86 87 // If true, TV going to standby mode puts other devices also to standby. 88 private boolean mAutoDeviceOff; 89 90 HdmiCecLocalDeviceTv(HdmiControlService service) { 91 super(service, HdmiCecDeviceInfo.DEVICE_TV); 92 mPrevPortId = Constants.INVALID_PORT_ID; 93 } 94 95 @Override 96 @ServiceThreadOnly 97 protected void onAddressAllocated(int logicalAddress) { 98 assertRunOnServiceThread(); 99 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 100 mAddress, mService.getPhysicalAddress(), mDeviceType)); 101 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 102 mAddress, mService.getVendorId())); 103 mSystemAudioMode = mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false); 104 launchRoutingControl(true); 105 launchDeviceDiscovery(); 106 registerAudioPortUpdateListener(); 107 // TODO: unregister audio port update listener if local device is released. 108 } 109 110 private void registerAudioPortUpdateListener() { 111 mService.getAudioManager().registerAudioPortUpdateListener( 112 new OnAudioPortUpdateListener() { 113 @Override 114 public void OnAudioPatchListUpdate(AudioPatch[] patchList) {} 115 116 @Override 117 public void OnAudioPortListUpdate(AudioPort[] portList) { 118 if (!mSystemAudioMode) { 119 return; 120 } 121 int devices = mService.getAudioManager().getDevicesForStream( 122 AudioSystem.STREAM_MUSIC); 123 if ((devices & ~AudioSystem.DEVICE_ALL_HDMI_SYSTEM_AUDIO_AND_SPEAKER) 124 != 0) { 125 // TODO: release system audio here. 126 } 127 } 128 129 @Override 130 public void OnServiceDied() {} 131 }); 132 } 133 134 /** 135 * Performs the action 'device select', or 'one touch play' initiated by TV. 136 * 137 * @param targetAddress logical address of the device to select 138 * @param callback callback object to report the result with 139 */ 140 @ServiceThreadOnly 141 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 142 assertRunOnServiceThread(); 143 if (targetAddress == Constants.ADDR_INTERNAL) { 144 handleSelectInternalSource(callback); 145 return; 146 } 147 HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress); 148 if (targetDevice == null) { 149 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 150 return; 151 } 152 removeAction(DeviceSelectAction.class); 153 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 154 } 155 156 @ServiceThreadOnly 157 private void handleSelectInternalSource(IHdmiControlCallback callback) { 158 assertRunOnServiceThread(); 159 // Seq #18 160 if (mService.isControlEnabled() && getActiveSource() != mAddress) { 161 updateActiveSource(mAddress, mService.getPhysicalAddress()); 162 // TODO: Check if this comes from <Text/Image View On> - if true, do nothing. 163 HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource( 164 mAddress, mService.getPhysicalAddress()); 165 mService.sendCecCommand(activeSource); 166 } 167 } 168 169 @ServiceThreadOnly 170 void updateActiveSource(int activeSource, int activePath) { 171 assertRunOnServiceThread(); 172 // Seq #14 173 if (activeSource == getActiveSource() && activePath == getActivePath()) { 174 return; 175 } 176 setActiveSource(activeSource); 177 setActivePath(activePath); 178 if (getDeviceInfo(activeSource) != null && activeSource != mAddress) { 179 if (mService.pathToPortId(activePath) == getActivePortId()) { 180 setPrevPortId(getActivePortId()); 181 } 182 // TODO: Show the OSD banner related to the new active source device. 183 } else { 184 // TODO: If displayed, remove the OSD banner related to the previous 185 // active source device. 186 } 187 } 188 189 /** 190 * Returns the previous port id kept to handle input switching on <Inactive Source>. 191 */ 192 int getPrevPortId() { 193 synchronized (mLock) { 194 return mPrevPortId; 195 } 196 } 197 198 /** 199 * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be 200 * taken for <Inactive Source>. 201 */ 202 void setPrevPortId(int portId) { 203 synchronized (mLock) { 204 mPrevPortId = portId; 205 } 206 } 207 208 @ServiceThreadOnly 209 void updateActivePortId(int portId) { 210 assertRunOnServiceThread(); 211 // Seq #15 212 if (portId == getActivePortId()) { 213 return; 214 } 215 setPrevPortId(portId); 216 // TODO: Actually switch the physical port here. Handle PAP/PIP as well. 217 // Show OSD port change banner 218 mService.invokeInputChangeListener(getActiveSource()); 219 } 220 221 @ServiceThreadOnly 222 void doManualPortSwitching(int portId, IHdmiControlCallback callback) { 223 assertRunOnServiceThread(); 224 // Seq #20 225 if (!mService.isControlEnabled() || portId == getActivePortId()) { 226 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 227 return; 228 } 229 // TODO: Make sure this call does not stem from <Active Source> message reception. 230 231 setActivePortId(portId); 232 // TODO: Return immediately if the operation is triggered by <Text/Image View On> 233 // and this is the first notification about the active input after power-on. 234 // TODO: Handle invalid port id / active input which should be treated as an 235 // internal tuner. 236 237 removeAction(RoutingControlAction.class); 238 239 int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId())); 240 int newPath = mService.portIdToPath(portId); 241 HdmiCecMessage routingChange = 242 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 243 mService.sendCecCommand(routingChange); 244 addAndStartAction(new RoutingControlAction(this, newPath, false, callback)); 245 } 246 247 int getPowerStatus() { 248 return mService.getPowerStatus(); 249 } 250 251 /** 252 * Sends key to a target CEC device. 253 * 254 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 255 * @param isPressed true if this is key press event 256 */ 257 @Override 258 @ServiceThreadOnly 259 protected void sendKeyEvent(int keyCode, boolean isPressed) { 260 assertRunOnServiceThread(); 261 List<SendKeyAction> action = getActions(SendKeyAction.class); 262 if (!action.isEmpty()) { 263 action.get(0).processKeyEvent(keyCode, isPressed); 264 } else { 265 if (isPressed) { 266 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 267 } else { 268 Slog.w(TAG, "Discard key release event"); 269 } 270 } 271 } 272 273 private static void invokeCallback(IHdmiControlCallback callback, int result) { 274 if (callback == null) { 275 return; 276 } 277 try { 278 callback.onComplete(result); 279 } catch (RemoteException e) { 280 Slog.e(TAG, "Invoking callback failed:" + e); 281 } 282 } 283 284 @Override 285 @ServiceThreadOnly 286 protected boolean handleActiveSource(HdmiCecMessage message) { 287 assertRunOnServiceThread(); 288 int address = message.getSource(); 289 int path = HdmiUtils.twoBytesToInt(message.getParams()); 290 if (getDeviceInfo(address) == null) { 291 handleNewDeviceAtTheTailOfActivePath(path); 292 } else { 293 ActiveSourceHandler.create(this, null).process(address, path); 294 } 295 return true; 296 } 297 298 @Override 299 @ServiceThreadOnly 300 protected boolean handleInactiveSource(HdmiCecMessage message) { 301 assertRunOnServiceThread(); 302 // Seq #10 303 304 // Ignore <Inactive Source> from non-active source device. 305 if (getActiveSource() != message.getSource()) { 306 return true; 307 } 308 if (isProhibitMode()) { 309 return true; 310 } 311 int portId = getPrevPortId(); 312 if (portId != Constants.INVALID_PORT_ID) { 313 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 314 315 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 316 if (inactiveSource == null) { 317 return true; 318 } 319 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 320 return true; 321 } 322 // TODO: Switch the TV freeze mode off 323 324 setActivePortId(portId); 325 doManualPortSwitching(portId, null); 326 setPrevPortId(Constants.INVALID_PORT_ID); 327 } 328 return true; 329 } 330 331 @Override 332 @ServiceThreadOnly 333 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 334 assertRunOnServiceThread(); 335 // Seq #19 336 if (mAddress == getActiveSource()) { 337 mService.sendCecCommand( 338 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 339 } 340 return true; 341 } 342 343 @Override 344 @ServiceThreadOnly 345 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 346 assertRunOnServiceThread(); 347 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 348 mAddress, Locale.getDefault().getISO3Language()); 349 // TODO: figure out how to handle failed to get language code. 350 if (command != null) { 351 mService.sendCecCommand(command); 352 } else { 353 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 354 } 355 return true; 356 } 357 358 @Override 359 @ServiceThreadOnly 360 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 361 assertRunOnServiceThread(); 362 // Ignore if [Device Discovery Action] is going on. 363 if (hasAction(DeviceDiscoveryAction.class)) { 364 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 365 + "because Device Discovery Action is on-going:" + message); 366 return true; 367 } 368 369 int path = HdmiUtils.twoBytesToInt(message.getParams()); 370 int address = message.getSource(); 371 if (!isInDeviceList(path, address)) { 372 handleNewDeviceAtTheTailOfActivePath(path); 373 } 374 addAndStartAction(new NewDeviceAction(this, address, path)); 375 return true; 376 } 377 378 private void handleNewDeviceAtTheTailOfActivePath(int path) { 379 // Seq #22 380 if (isTailOfActivePath(path, getActivePath())) { 381 removeAction(RoutingControlAction.class); 382 int newPath = mService.portIdToPath(getActivePortId()); 383 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 384 mAddress, getActivePath(), newPath)); 385 addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null)); 386 } 387 } 388 389 /** 390 * Whether the given path is located in the tail of current active path. 391 * 392 * @param path to be tested 393 * @param activePath current active path 394 * @return true if the given path is located in the tail of current active path; otherwise, 395 * false 396 */ 397 static boolean isTailOfActivePath(int path, int activePath) { 398 // If active routing path is internal source, return false. 399 if (activePath == 0) { 400 return false; 401 } 402 for (int i = 12; i >= 0; i -= 4) { 403 int curActivePath = (activePath >> i) & 0xF; 404 if (curActivePath == 0) { 405 return true; 406 } else { 407 int curPath = (path >> i) & 0xF; 408 if (curPath != curActivePath) { 409 return false; 410 } 411 } 412 } 413 return false; 414 } 415 416 @Override 417 @ServiceThreadOnly 418 protected boolean handleRoutingChange(HdmiCecMessage message) { 419 assertRunOnServiceThread(); 420 // Seq #21 421 byte[] params = message.getParams(); 422 int currentPath = HdmiUtils.twoBytesToInt(params); 423 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 424 int newPath = HdmiUtils.twoBytesToInt(params, 2); 425 setActivePath(newPath); 426 removeAction(RoutingControlAction.class); 427 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 428 } 429 return true; 430 } 431 432 @Override 433 @ServiceThreadOnly 434 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 435 assertRunOnServiceThread(); 436 437 byte params[] = message.getParams(); 438 int mute = params[0] & 0x80; 439 int volume = params[0] & 0x7F; 440 setAudioStatus(mute == 0x80, volume); 441 return true; 442 } 443 444 @Override 445 @ServiceThreadOnly 446 protected boolean handleTextViewOn(HdmiCecMessage message) { 447 assertRunOnServiceThread(); 448 if (mService.isPowerStandbyOrTransient()) { 449 mService.wakeUp(); 450 } 451 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 452 // that represents the source device. 453 return true; 454 } 455 456 @Override 457 @ServiceThreadOnly 458 protected boolean handleImageViewOn(HdmiCecMessage message) { 459 assertRunOnServiceThread(); 460 // Currently, it's the same as <Text View On>. 461 return handleTextViewOn(message); 462 } 463 464 @Override 465 @ServiceThreadOnly 466 protected boolean handleSetOsdName(HdmiCecMessage message) { 467 int source = message.getSource(); 468 HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source); 469 // If the device is not in device list, ignore it. 470 if (deviceInfo == null) { 471 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 472 return true; 473 } 474 String osdName = null; 475 try { 476 osdName = new String(message.getParams(), "US-ASCII"); 477 } catch (UnsupportedEncodingException e) { 478 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 479 return true; 480 } 481 482 if (deviceInfo.getDisplayName().equals(osdName)) { 483 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 484 return true; 485 } 486 487 addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(), 488 deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(), 489 deviceInfo.getVendorId(), osdName)); 490 return true; 491 } 492 493 @ServiceThreadOnly 494 private void launchDeviceDiscovery() { 495 assertRunOnServiceThread(); 496 clearDeviceInfoList(); 497 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 498 new DeviceDiscoveryCallback() { 499 @Override 500 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 501 for (HdmiCecDeviceInfo info : deviceInfos) { 502 addCecDevice(info); 503 } 504 505 // Since we removed all devices when it's start and 506 // device discovery action does not poll local devices, 507 // we should put device info of local device manually here 508 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 509 addCecDevice(device.getDeviceInfo()); 510 } 511 512 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 513 514 // If there is AVR, initiate System Audio Auto initiation action, 515 // which turns on and off system audio according to last system 516 // audio setting. 517 if (mSystemAudioMode && getAvrDeviceInfo() != null) { 518 addAndStartAction(new SystemAudioAutoInitiationAction( 519 HdmiCecLocalDeviceTv.this, 520 getAvrDeviceInfo().getLogicalAddress())); 521 if (mArcEstablished) { 522 startArcAction(true); 523 } 524 } 525 } 526 }); 527 addAndStartAction(action); 528 } 529 530 // Clear all device info. 531 @ServiceThreadOnly 532 private void clearDeviceInfoList() { 533 assertRunOnServiceThread(); 534 mDeviceInfos.clear(); 535 updateSafeDeviceInfoList(); 536 } 537 538 @ServiceThreadOnly 539 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 540 assertRunOnServiceThread(); 541 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 542 if (avr == null) { 543 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 544 return; 545 } 546 547 addAndStartAction( 548 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 549 } 550 551 // # Seq 25 552 void setSystemAudioMode(boolean on, boolean updateSetting) { 553 synchronized (mLock) { 554 if (on != mSystemAudioMode) { 555 mSystemAudioMode = on; 556 if (updateSetting) { 557 mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on); 558 } 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), true); 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 disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1068 super.disableDevice(initiatedByCec, callback); 1069 assertRunOnServiceThread(); 1070 // Remove any repeated working actions. 1071 // HotplugDetectionAction will be reinstated during the wake up process. 1072 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1073 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1074 removeAction(DeviceDiscoveryAction.class); 1075 removeAction(HotplugDetectionAction.class); 1076 1077 disableSystemAudioIfExist(); 1078 disableArcIfExist(); 1079 checkIfPendingActionsCleared(); 1080 } 1081 1082 @ServiceThreadOnly 1083 private void disableSystemAudioIfExist() { 1084 assertRunOnServiceThread(); 1085 if (getAvrDeviceInfo() == null) { 1086 return; 1087 } 1088 1089 // Seq #31. 1090 removeAction(SystemAudioActionFromAvr.class); 1091 removeAction(SystemAudioActionFromTv.class); 1092 removeAction(SystemAudioAutoInitiationAction.class); 1093 removeAction(SystemAudioStatusAction.class); 1094 removeAction(VolumeControlAction.class); 1095 1096 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1097 // the system audio mode setting can be restored automatically. 1098 setSystemAudioMode(false, false); 1099 } 1100 1101 @ServiceThreadOnly 1102 private void disableArcIfExist() { 1103 assertRunOnServiceThread(); 1104 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 1105 if (avr == null) { 1106 return; 1107 } 1108 1109 // Seq #44. 1110 removeAction(RequestArcInitiationAction.class); 1111 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1112 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1113 } 1114 } 1115 1116 @Override 1117 @ServiceThreadOnly 1118 protected void onStandby(boolean initiatedByCec) { 1119 assertRunOnServiceThread(); 1120 // Seq #11 1121 if (!mService.isControlEnabled()) { 1122 return; 1123 } 1124 if (!initiatedByCec) { 1125 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1126 mAddress, Constants.ADDR_BROADCAST)); 1127 } 1128 } 1129 1130 @Override 1131 @ServiceThreadOnly 1132 protected boolean handleStandby(HdmiCecMessage message) { 1133 assertRunOnServiceThread(); 1134 // Seq #12 1135 // Tv accepts directly addressed <Standby> only. 1136 if (message.getDestination() == mAddress) { 1137 super.handleStandby(message); 1138 } 1139 return false; 1140 } 1141 1142 boolean isProhibitMode() { 1143 return mService.isProhibitMode(); 1144 } 1145 1146 boolean isPowerStandbyOrTransient() { 1147 return mService.isPowerStandbyOrTransient(); 1148 } 1149 1150 void displayOsd(int messageId) { 1151 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1152 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1153 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1154 HdmiControlService.PERMISSION); 1155 } 1156} 1157