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