HdmiCecLocalDeviceTv.java revision c516d65fd96cdc39f9935ddb80d26ee6499a77bf
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, boolean fromBootup) { 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(fromBootup); 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 // Seq #32 540 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 541 assertRunOnServiceThread(); 542 if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) { 543 setSystemAudioMode(false, true); 544 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 545 return; 546 } 547 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 548 if (avr == null) { 549 setSystemAudioMode(false, true); 550 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 551 return; 552 } 553 554 addAndStartAction( 555 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 556 } 557 558 // # Seq 25 559 void setSystemAudioMode(boolean on, boolean updateSetting) { 560 synchronized (mLock) { 561 if (on != mSystemAudioMode) { 562 mSystemAudioMode = on; 563 if (updateSetting) { 564 mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on); 565 } 566 mService.announceSystemAudioModeChange(on); 567 } 568 } 569 } 570 571 boolean getSystemAudioMode() { 572 synchronized (mLock) { 573 return mSystemAudioMode; 574 } 575 } 576 577 /** 578 * Change ARC status into the given {@code enabled} status. 579 * 580 * @return {@code true} if ARC was in "Enabled" status 581 */ 582 @ServiceThreadOnly 583 boolean setArcStatus(boolean enabled) { 584 assertRunOnServiceThread(); 585 boolean oldStatus = mArcEstablished; 586 // 1. Enable/disable ARC circuit. 587 mService.setAudioReturnChannel(enabled); 588 // 2. Notify arc status to audio service. 589 notifyArcStatusToAudioService(enabled); 590 // 3. Update arc status; 591 mArcEstablished = enabled; 592 return oldStatus; 593 } 594 595 private void notifyArcStatusToAudioService(boolean enabled) { 596 // Note that we don't set any name to ARC. 597 mService.getAudioManager().setWiredDeviceConnectionState( 598 AudioSystem.DEVICE_OUT_HDMI_ARC, 599 enabled ? 1 : 0, ""); 600 } 601 602 /** 603 * Returns whether ARC is enabled or not. 604 */ 605 @ServiceThreadOnly 606 boolean isArcEstabilished() { 607 assertRunOnServiceThread(); 608 return mArcFeatureEnabled && mArcEstablished; 609 } 610 611 @ServiceThreadOnly 612 void changeArcFeatureEnabled(boolean enabled) { 613 assertRunOnServiceThread(); 614 615 if (mArcFeatureEnabled != enabled) { 616 if (enabled) { 617 if (!mArcEstablished) { 618 startArcAction(true); 619 } 620 } else { 621 if (mArcEstablished) { 622 startArcAction(false); 623 } 624 } 625 mArcFeatureEnabled = enabled; 626 } 627 } 628 629 @ServiceThreadOnly 630 boolean isArcFeatureEnabled() { 631 assertRunOnServiceThread(); 632 return mArcFeatureEnabled; 633 } 634 635 @ServiceThreadOnly 636 private void startArcAction(boolean enabled) { 637 assertRunOnServiceThread(); 638 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 639 if (info == null) { 640 return; 641 } 642 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 643 return; 644 } 645 646 // Terminate opposite action and start action if not exist. 647 if (enabled) { 648 removeAction(RequestArcTerminationAction.class); 649 if (!hasAction(RequestArcInitiationAction.class)) { 650 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 651 } 652 } else { 653 removeAction(RequestArcInitiationAction.class); 654 if (!hasAction(RequestArcTerminationAction.class)) { 655 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 656 } 657 } 658 } 659 660 void setAudioStatus(boolean mute, int volume) { 661 synchronized (mLock) { 662 mSystemAudioMute = mute; 663 mSystemAudioVolume = volume; 664 // TODO: pass volume to service (audio service) after scale it to local volume level. 665 mService.setAudioStatus(mute, volume); 666 } 667 } 668 669 @ServiceThreadOnly 670 void changeVolume(int curVolume, int delta, int maxVolume) { 671 assertRunOnServiceThread(); 672 if (delta == 0 || !isSystemAudioOn()) { 673 return; 674 } 675 676 int targetVolume = curVolume + delta; 677 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 678 synchronized (mLock) { 679 // If new volume is the same as current system audio volume, just ignore it. 680 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 681 if (cecVolume == mSystemAudioVolume) { 682 // Update tv volume with system volume value. 683 mService.setAudioStatus(false, 684 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 685 return; 686 } 687 } 688 689 // Remove existing volume action. 690 removeAction(VolumeControlAction.class); 691 692 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 693 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 694 cecVolume, delta > 0)); 695 } 696 697 @ServiceThreadOnly 698 void changeMute(boolean mute) { 699 assertRunOnServiceThread(); 700 if (!isSystemAudioOn()) { 701 return; 702 } 703 704 // Remove existing volume action. 705 removeAction(VolumeControlAction.class); 706 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 707 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 708 } 709 710 private boolean isSystemAudioOn() { 711 if (getAvrDeviceInfo() == null) { 712 return false; 713 } 714 715 synchronized (mLock) { 716 return mSystemAudioMode; 717 } 718 } 719 720 @Override 721 @ServiceThreadOnly 722 protected boolean handleInitiateArc(HdmiCecMessage message) { 723 assertRunOnServiceThread(); 724 // In case where <Initiate Arc> is started by <Request ARC Initiation> 725 // need to clean up RequestArcInitiationAction. 726 removeAction(RequestArcInitiationAction.class); 727 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 728 message.getSource(), true); 729 addAndStartAction(action); 730 return true; 731 } 732 733 @Override 734 @ServiceThreadOnly 735 protected boolean handleTerminateArc(HdmiCecMessage message) { 736 assertRunOnServiceThread(); 737 // In case where <Terminate Arc> is started by <Request ARC Termination> 738 // need to clean up RequestArcInitiationAction. 739 removeAction(RequestArcTerminationAction.class); 740 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 741 message.getSource(), false); 742 addAndStartAction(action); 743 return true; 744 } 745 746 @Override 747 @ServiceThreadOnly 748 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 749 assertRunOnServiceThread(); 750 if (!isMessageForSystemAudio(message)) { 751 return false; 752 } 753 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 754 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 755 addAndStartAction(action); 756 return true; 757 } 758 759 @Override 760 @ServiceThreadOnly 761 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 762 assertRunOnServiceThread(); 763 if (!isMessageForSystemAudio(message)) { 764 return false; 765 } 766 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true); 767 return true; 768 } 769 770 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 771 if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM 772 || message.getDestination() != Constants.ADDR_TV 773 || getAvrDeviceInfo() == null) { 774 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 775 return false; 776 } 777 return true; 778 } 779 780 /** 781 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 782 * logical address as new device info's. 783 * 784 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 785 * 786 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 787 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 788 * that has the same logical address as new one has. 789 */ 790 @ServiceThreadOnly 791 private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 792 assertRunOnServiceThread(); 793 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 794 if (oldDeviceInfo != null) { 795 removeDeviceInfo(deviceInfo.getLogicalAddress()); 796 } 797 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 798 updateSafeDeviceInfoList(); 799 return oldDeviceInfo; 800 } 801 802 /** 803 * Remove a device info corresponding to the given {@code logicalAddress}. 804 * It returns removed {@link HdmiCecDeviceInfo} if exists. 805 * 806 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 807 * 808 * @param logicalAddress logical address of device to be removed 809 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 810 */ 811 @ServiceThreadOnly 812 private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 813 assertRunOnServiceThread(); 814 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 815 if (deviceInfo != null) { 816 mDeviceInfos.remove(logicalAddress); 817 } 818 updateSafeDeviceInfoList(); 819 return deviceInfo; 820 } 821 822 /** 823 * Return a list of all {@link HdmiCecDeviceInfo}. 824 * 825 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 826 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 827 */ 828 @ServiceThreadOnly 829 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 830 assertRunOnServiceThread(); 831 if (includelLocalDevice) { 832 return HdmiUtils.sparseArrayToList(mDeviceInfos); 833 } else { 834 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 835 for (int i = 0; i < mDeviceInfos.size(); ++i) { 836 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 837 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 838 infoList.add(info); 839 } 840 } 841 return infoList; 842 } 843 } 844 845 /** 846 * Return external input devices. 847 */ 848 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 849 synchronized (mLock) { 850 return mSafeExternalInputs; 851 } 852 } 853 854 @ServiceThreadOnly 855 private void updateSafeDeviceInfoList() { 856 assertRunOnServiceThread(); 857 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 858 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 859 synchronized (mLock) { 860 mSafeAllDeviceInfos = copiedDevices; 861 mSafeExternalInputs = externalInputs; 862 } 863 } 864 865 /** 866 * Return a list of external cec input (source) devices. 867 * 868 * <p>Note that this effectively excludes non-source devices like system audio, 869 * secondary TV. 870 */ 871 private List<HdmiCecDeviceInfo> getInputDevices() { 872 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 873 for (int i = 0; i < mDeviceInfos.size(); ++i) { 874 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 875 if (isLocalDeviceAddress(i)) { 876 continue; 877 } 878 if (info.isSourceType()) { 879 infoList.add(info); 880 } 881 } 882 return infoList; 883 } 884 885 @ServiceThreadOnly 886 private boolean isLocalDeviceAddress(int address) { 887 assertRunOnServiceThread(); 888 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 889 if (device.isAddressOf(address)) { 890 return true; 891 } 892 } 893 return false; 894 } 895 896 @ServiceThreadOnly 897 HdmiCecDeviceInfo getAvrDeviceInfo() { 898 assertRunOnServiceThread(); 899 return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 900 } 901 902 /** 903 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 904 * 905 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 906 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 907 * 908 * @param logicalAddress logical address to be retrieved 909 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 910 * Returns null if no logical address matched 911 */ 912 @ServiceThreadOnly 913 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 914 assertRunOnServiceThread(); 915 return mDeviceInfos.get(logicalAddress); 916 } 917 918 boolean hasSystemAudioDevice() { 919 return getSafeAvrDeviceInfo() != null; 920 } 921 922 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 923 return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 924 } 925 926 /** 927 * Thread safe version of {@link #getDeviceInfo(int)}. 928 * 929 * @param logicalAddress logical address to be retrieved 930 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 931 * Returns null if no logical address matched 932 */ 933 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 934 synchronized (mLock) { 935 return mSafeAllDeviceInfos.get(logicalAddress); 936 } 937 } 938 939 /** 940 * Called when a device is newly added or a new device is detected or 941 * existing device is updated. 942 * 943 * @param info device info of a new device. 944 */ 945 @ServiceThreadOnly 946 final void addCecDevice(HdmiCecDeviceInfo info) { 947 assertRunOnServiceThread(); 948 addDeviceInfo(info); 949 if (info.getLogicalAddress() == mAddress) { 950 // The addition of TV device itself should not be notified. 951 return; 952 } 953 mService.invokeDeviceEventListeners(info, true); 954 } 955 956 /** 957 * Called when a device is removed or removal of device is detected. 958 * 959 * @param address a logical address of a device to be removed 960 */ 961 @ServiceThreadOnly 962 final void removeCecDevice(int address) { 963 assertRunOnServiceThread(); 964 HdmiCecDeviceInfo info = removeDeviceInfo(address); 965 966 mCecMessageCache.flushMessagesFrom(address); 967 mService.invokeDeviceEventListeners(info, false); 968 } 969 970 @ServiceThreadOnly 971 void handleRemoveActiveRoutingPath(int path) { 972 assertRunOnServiceThread(); 973 // Seq #23 974 if (isTailOfActivePath(path, getActivePath())) { 975 removeAction(RoutingControlAction.class); 976 int newPath = mService.portIdToPath(getActivePortId()); 977 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 978 mAddress, getActivePath(), newPath)); 979 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 980 } 981 } 982 983 /** 984 * Launch routing control process. 985 * 986 * @param routingForBootup true if routing control is initiated due to One Touch Play 987 * or TV power on 988 */ 989 @ServiceThreadOnly 990 void launchRoutingControl(boolean routingForBootup) { 991 assertRunOnServiceThread(); 992 // Seq #24 993 if (getActivePortId() != Constants.INVALID_PORT_ID) { 994 if (!routingForBootup && !isProhibitMode()) { 995 removeAction(RoutingControlAction.class); 996 int newPath = mService.portIdToPath(getActivePortId()); 997 setActivePath(newPath); 998 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 999 getActivePath(), newPath)); 1000 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 1001 routingForBootup, null)); 1002 } 1003 } else { 1004 int activePath = mService.getPhysicalAddress(); 1005 setActivePath(activePath); 1006 if (!routingForBootup) { 1007 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1008 activePath)); 1009 } 1010 } 1011 } 1012 1013 /** 1014 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 1015 * the given routing path. CEC devices use routing path for its physical address to 1016 * describe the hierarchy of the devices in the network. 1017 * 1018 * @param path routing path or physical address 1019 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 1020 */ 1021 @ServiceThreadOnly 1022 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 1023 assertRunOnServiceThread(); 1024 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 1025 if (info.getPhysicalAddress() == path) { 1026 return info; 1027 } 1028 } 1029 return null; 1030 } 1031 1032 /** 1033 * Whether a device of the specified physical address and logical address exists 1034 * in a device info list. However, both are minimal condition and it could 1035 * be different device from the original one. 1036 * 1037 * @param logicalAddress logical address of a device to be searched 1038 * @param physicalAddress physical address of a device to be searched 1039 * @return true if exist; otherwise false 1040 */ 1041 @ServiceThreadOnly 1042 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1043 assertRunOnServiceThread(); 1044 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1045 if (device == null) { 1046 return false; 1047 } 1048 return device.getPhysicalAddress() == physicalAddress; 1049 } 1050 1051 @Override 1052 @ServiceThreadOnly 1053 void onHotplug(int portId, boolean connected) { 1054 assertRunOnServiceThread(); 1055 1056 // Tv device will have permanent HotplugDetectionAction. 1057 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1058 if (!hotplugActions.isEmpty()) { 1059 // Note that hotplug action is single action running on a machine. 1060 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1061 // It covers seq #40, #43. 1062 hotplugActions.get(0).pollAllDevicesNow(); 1063 } 1064 } 1065 1066 @ServiceThreadOnly 1067 void setAutoDeviceOff(boolean enabled) { 1068 assertRunOnServiceThread(); 1069 mAutoDeviceOff = enabled; 1070 } 1071 1072 @Override 1073 @ServiceThreadOnly 1074 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1075 super.disableDevice(initiatedByCec, callback); 1076 assertRunOnServiceThread(); 1077 // Remove any repeated working actions. 1078 // HotplugDetectionAction will be reinstated during the wake up process. 1079 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1080 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1081 removeAction(DeviceDiscoveryAction.class); 1082 removeAction(HotplugDetectionAction.class); 1083 1084 disableSystemAudioIfExist(); 1085 disableArcIfExist(); 1086 checkIfPendingActionsCleared(); 1087 } 1088 1089 @ServiceThreadOnly 1090 private void disableSystemAudioIfExist() { 1091 assertRunOnServiceThread(); 1092 if (getAvrDeviceInfo() == null) { 1093 return; 1094 } 1095 1096 // Seq #31. 1097 removeAction(SystemAudioActionFromAvr.class); 1098 removeAction(SystemAudioActionFromTv.class); 1099 removeAction(SystemAudioAutoInitiationAction.class); 1100 removeAction(SystemAudioStatusAction.class); 1101 removeAction(VolumeControlAction.class); 1102 1103 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1104 // the system audio mode setting can be restored automatically. 1105 setSystemAudioMode(false, false); 1106 } 1107 1108 @ServiceThreadOnly 1109 private void disableArcIfExist() { 1110 assertRunOnServiceThread(); 1111 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 1112 if (avr == null) { 1113 return; 1114 } 1115 1116 // Seq #44. 1117 removeAction(RequestArcInitiationAction.class); 1118 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1119 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1120 } 1121 } 1122 1123 @Override 1124 @ServiceThreadOnly 1125 protected void onStandby(boolean initiatedByCec) { 1126 assertRunOnServiceThread(); 1127 // Seq #11 1128 if (!mService.isControlEnabled()) { 1129 return; 1130 } 1131 if (!initiatedByCec) { 1132 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1133 mAddress, Constants.ADDR_BROADCAST)); 1134 } 1135 } 1136 1137 @Override 1138 @ServiceThreadOnly 1139 protected boolean handleStandby(HdmiCecMessage message) { 1140 assertRunOnServiceThread(); 1141 // Seq #12 1142 // Tv accepts directly addressed <Standby> only. 1143 if (message.getDestination() == mAddress) { 1144 super.handleStandby(message); 1145 } 1146 return false; 1147 } 1148 1149 boolean isProhibitMode() { 1150 return mService.isProhibitMode(); 1151 } 1152 1153 boolean isPowerStandbyOrTransient() { 1154 return mService.isPowerStandbyOrTransient(); 1155 } 1156 1157 void displayOsd(int messageId) { 1158 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1159 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1160 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1161 HdmiControlService.PERMISSION); 1162 } 1163} 1164