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