HdmiCecLocalDeviceTv.java revision 97affee67b6d88da40af41b36f02ecb2b823daff
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 startNewDeviceAction(address, path); 403 return true; 404 } 405 406 void startNewDeviceAction(int address, int path) { 407 for (NewDeviceAction action : getActions(NewDeviceAction.class)) { 408 // If there is new device action which has the same logical address and path 409 // ignore new request. 410 // NewDeviceAction is created whenever it receives <Report Physical Address>. 411 // And there is a chance starting NewDeviceAction for the same source. 412 // Usually, new device sends <Report Physical Address> when it's plugged 413 // in. However, TV can detect a new device from HotPlugDetectionAction, 414 // which sends <Give Physical Address> to the source for newly detected 415 // device. 416 if (action.isActionOf(address, path)) { 417 return; 418 } 419 } 420 421 addAndStartAction(new NewDeviceAction(this, address, path)); 422 } 423 424 private void handleNewDeviceAtTheTailOfActivePath(int path) { 425 // Seq #22 426 if (isTailOfActivePath(path, getActivePath())) { 427 removeAction(RoutingControlAction.class); 428 int newPath = mService.portIdToPath(getActivePortId()); 429 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 430 mAddress, getActivePath(), newPath)); 431 addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null)); 432 } 433 } 434 435 /** 436 * Whether the given path is located in the tail of current active path. 437 * 438 * @param path to be tested 439 * @param activePath current active path 440 * @return true if the given path is located in the tail of current active path; otherwise, 441 * false 442 */ 443 static boolean isTailOfActivePath(int path, int activePath) { 444 // If active routing path is internal source, return false. 445 if (activePath == 0) { 446 return false; 447 } 448 for (int i = 12; i >= 0; i -= 4) { 449 int curActivePath = (activePath >> i) & 0xF; 450 if (curActivePath == 0) { 451 return true; 452 } else { 453 int curPath = (path >> i) & 0xF; 454 if (curPath != curActivePath) { 455 return false; 456 } 457 } 458 } 459 return false; 460 } 461 462 @Override 463 @ServiceThreadOnly 464 protected boolean handleRoutingChange(HdmiCecMessage message) { 465 assertRunOnServiceThread(); 466 // Seq #21 467 byte[] params = message.getParams(); 468 int currentPath = HdmiUtils.twoBytesToInt(params); 469 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 470 int newPath = HdmiUtils.twoBytesToInt(params, 2); 471 setActivePath(newPath); 472 removeAction(RoutingControlAction.class); 473 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 474 } 475 return true; 476 } 477 478 @Override 479 @ServiceThreadOnly 480 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 481 assertRunOnServiceThread(); 482 483 byte params[] = message.getParams(); 484 int mute = params[0] & 0x80; 485 int volume = params[0] & 0x7F; 486 setAudioStatus(mute == 0x80, volume); 487 return true; 488 } 489 490 @Override 491 @ServiceThreadOnly 492 protected boolean handleTextViewOn(HdmiCecMessage message) { 493 assertRunOnServiceThread(); 494 if (mService.isPowerStandbyOrTransient()) { 495 mService.wakeUp(); 496 } 497 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 498 // that represents the source device. 499 return true; 500 } 501 502 @Override 503 @ServiceThreadOnly 504 protected boolean handleImageViewOn(HdmiCecMessage message) { 505 assertRunOnServiceThread(); 506 // Currently, it's the same as <Text View On>. 507 return handleTextViewOn(message); 508 } 509 510 @Override 511 @ServiceThreadOnly 512 protected boolean handleSetOsdName(HdmiCecMessage message) { 513 int source = message.getSource(); 514 HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source); 515 // If the device is not in device list, ignore it. 516 if (deviceInfo == null) { 517 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 518 return true; 519 } 520 String osdName = null; 521 try { 522 osdName = new String(message.getParams(), "US-ASCII"); 523 } catch (UnsupportedEncodingException e) { 524 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 525 return true; 526 } 527 528 if (deviceInfo.getDisplayName().equals(osdName)) { 529 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 530 return true; 531 } 532 533 addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(), 534 deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(), 535 deviceInfo.getVendorId(), osdName)); 536 return true; 537 } 538 539 @ServiceThreadOnly 540 private void launchDeviceDiscovery() { 541 assertRunOnServiceThread(); 542 clearDeviceInfoList(); 543 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 544 new DeviceDiscoveryCallback() { 545 @Override 546 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 547 for (HdmiCecDeviceInfo info : deviceInfos) { 548 addCecDevice(info); 549 } 550 551 // Since we removed all devices when it's start and 552 // device discovery action does not poll local devices, 553 // we should put device info of local device manually here 554 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 555 addCecDevice(device.getDeviceInfo()); 556 } 557 558 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 559 560 // If there is AVR, initiate System Audio Auto initiation action, 561 // which turns on and off system audio according to last system 562 // audio setting. 563 if (mSystemAudioMode && getAvrDeviceInfo() != null) { 564 addAndStartAction(new SystemAudioAutoInitiationAction( 565 HdmiCecLocalDeviceTv.this, 566 getAvrDeviceInfo().getLogicalAddress())); 567 if (mArcEstablished) { 568 startArcAction(true); 569 } 570 } 571 } 572 }); 573 addAndStartAction(action); 574 } 575 576 // Clear all device info. 577 @ServiceThreadOnly 578 private void clearDeviceInfoList() { 579 assertRunOnServiceThread(); 580 mDeviceInfos.clear(); 581 updateSafeDeviceInfoList(); 582 } 583 584 @ServiceThreadOnly 585 // Seq #32 586 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 587 assertRunOnServiceThread(); 588 if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) { 589 setSystemAudioMode(false, true); 590 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 591 return; 592 } 593 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 594 if (avr == null) { 595 setSystemAudioMode(false, true); 596 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 597 return; 598 } 599 600 addAndStartAction( 601 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 602 } 603 604 // # Seq 25 605 void setSystemAudioMode(boolean on, boolean updateSetting) { 606 synchronized (mLock) { 607 if (on != mSystemAudioMode) { 608 mSystemAudioMode = on; 609 if (updateSetting) { 610 mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on); 611 } 612 mService.announceSystemAudioModeChange(on); 613 } 614 } 615 } 616 617 boolean getSystemAudioMode() { 618 synchronized (mLock) { 619 return mSystemAudioMode; 620 } 621 } 622 623 /** 624 * Change ARC status into the given {@code enabled} status. 625 * 626 * @return {@code true} if ARC was in "Enabled" status 627 */ 628 @ServiceThreadOnly 629 boolean setArcStatus(boolean enabled) { 630 assertRunOnServiceThread(); 631 boolean oldStatus = mArcEstablished; 632 // 1. Enable/disable ARC circuit. 633 mService.setAudioReturnChannel(enabled); 634 // 2. Notify arc status to audio service. 635 notifyArcStatusToAudioService(enabled); 636 // 3. Update arc status; 637 mArcEstablished = enabled; 638 return oldStatus; 639 } 640 641 private void notifyArcStatusToAudioService(boolean enabled) { 642 // Note that we don't set any name to ARC. 643 mService.getAudioManager().setWiredDeviceConnectionState( 644 AudioSystem.DEVICE_OUT_HDMI_ARC, 645 enabled ? 1 : 0, ""); 646 } 647 648 /** 649 * Returns whether ARC is enabled or not. 650 */ 651 @ServiceThreadOnly 652 boolean isArcEstabilished() { 653 assertRunOnServiceThread(); 654 return mArcFeatureEnabled && mArcEstablished; 655 } 656 657 @ServiceThreadOnly 658 void changeArcFeatureEnabled(boolean enabled) { 659 assertRunOnServiceThread(); 660 661 if (mArcFeatureEnabled != enabled) { 662 if (enabled) { 663 if (!mArcEstablished) { 664 startArcAction(true); 665 } 666 } else { 667 if (mArcEstablished) { 668 startArcAction(false); 669 } 670 } 671 mArcFeatureEnabled = enabled; 672 } 673 } 674 675 @ServiceThreadOnly 676 boolean isArcFeatureEnabled() { 677 assertRunOnServiceThread(); 678 return mArcFeatureEnabled; 679 } 680 681 @ServiceThreadOnly 682 private void startArcAction(boolean enabled) { 683 assertRunOnServiceThread(); 684 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 685 if (info == null) { 686 return; 687 } 688 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 689 return; 690 } 691 692 // Terminate opposite action and start action if not exist. 693 if (enabled) { 694 removeAction(RequestArcTerminationAction.class); 695 if (!hasAction(RequestArcInitiationAction.class)) { 696 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 697 } 698 } else { 699 removeAction(RequestArcInitiationAction.class); 700 if (!hasAction(RequestArcTerminationAction.class)) { 701 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 702 } 703 } 704 } 705 706 void setAudioStatus(boolean mute, int volume) { 707 synchronized (mLock) { 708 mSystemAudioMute = mute; 709 mSystemAudioVolume = volume; 710 int maxVolume = mService.getAudioManager().getStreamMaxVolume( 711 AudioManager.STREAM_MUSIC); 712 mService.setAudioStatus(mute, 713 VolumeControlAction.scaleToCustomVolume(volume, maxVolume)); 714 } 715 } 716 717 @ServiceThreadOnly 718 void changeVolume(int curVolume, int delta, int maxVolume) { 719 assertRunOnServiceThread(); 720 if (delta == 0 || !isSystemAudioOn()) { 721 return; 722 } 723 724 int targetVolume = curVolume + delta; 725 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 726 synchronized (mLock) { 727 // If new volume is the same as current system audio volume, just ignore it. 728 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 729 if (cecVolume == mSystemAudioVolume) { 730 // Update tv volume with system volume value. 731 mService.setAudioStatus(false, 732 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 733 return; 734 } 735 } 736 737 // Remove existing volume action. 738 removeAction(VolumeControlAction.class); 739 740 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 741 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 742 cecVolume, delta > 0)); 743 } 744 745 @ServiceThreadOnly 746 void changeMute(boolean mute) { 747 assertRunOnServiceThread(); 748 if (!isSystemAudioOn()) { 749 return; 750 } 751 752 // Remove existing volume action. 753 removeAction(VolumeControlAction.class); 754 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 755 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 756 } 757 758 private boolean isSystemAudioOn() { 759 if (getAvrDeviceInfo() == null) { 760 return false; 761 } 762 763 synchronized (mLock) { 764 return mSystemAudioMode; 765 } 766 } 767 768 @Override 769 @ServiceThreadOnly 770 protected boolean handleInitiateArc(HdmiCecMessage message) { 771 assertRunOnServiceThread(); 772 // In case where <Initiate Arc> is started by <Request ARC Initiation> 773 // need to clean up RequestArcInitiationAction. 774 removeAction(RequestArcInitiationAction.class); 775 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 776 message.getSource(), true); 777 addAndStartAction(action); 778 return true; 779 } 780 781 @Override 782 @ServiceThreadOnly 783 protected boolean handleTerminateArc(HdmiCecMessage message) { 784 assertRunOnServiceThread(); 785 // In case where <Terminate Arc> is started by <Request ARC Termination> 786 // need to clean up RequestArcInitiationAction. 787 removeAction(RequestArcTerminationAction.class); 788 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 789 message.getSource(), false); 790 addAndStartAction(action); 791 return true; 792 } 793 794 @Override 795 @ServiceThreadOnly 796 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 797 assertRunOnServiceThread(); 798 if (!isMessageForSystemAudio(message)) { 799 return false; 800 } 801 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 802 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 803 addAndStartAction(action); 804 return true; 805 } 806 807 @Override 808 @ServiceThreadOnly 809 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 810 assertRunOnServiceThread(); 811 if (!isMessageForSystemAudio(message)) { 812 return false; 813 } 814 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true); 815 return true; 816 } 817 818 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 819 if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM 820 || message.getDestination() != Constants.ADDR_TV 821 || getAvrDeviceInfo() == null) { 822 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 823 return false; 824 } 825 return true; 826 } 827 828 /** 829 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 830 * logical address as new device info's. 831 * 832 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 833 * 834 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 835 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 836 * that has the same logical address as new one has. 837 */ 838 @ServiceThreadOnly 839 private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 840 assertRunOnServiceThread(); 841 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 842 if (oldDeviceInfo != null) { 843 removeDeviceInfo(deviceInfo.getLogicalAddress()); 844 } 845 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 846 updateSafeDeviceInfoList(); 847 return oldDeviceInfo; 848 } 849 850 /** 851 * Remove a device info corresponding to the given {@code logicalAddress}. 852 * It returns removed {@link HdmiCecDeviceInfo} if exists. 853 * 854 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 855 * 856 * @param logicalAddress logical address of device to be removed 857 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 858 */ 859 @ServiceThreadOnly 860 private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 861 assertRunOnServiceThread(); 862 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 863 if (deviceInfo != null) { 864 mDeviceInfos.remove(logicalAddress); 865 } 866 updateSafeDeviceInfoList(); 867 return deviceInfo; 868 } 869 870 /** 871 * Return a list of all {@link HdmiCecDeviceInfo}. 872 * 873 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 874 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 875 */ 876 @ServiceThreadOnly 877 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 878 assertRunOnServiceThread(); 879 if (includelLocalDevice) { 880 return HdmiUtils.sparseArrayToList(mDeviceInfos); 881 } else { 882 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 883 for (int i = 0; i < mDeviceInfos.size(); ++i) { 884 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 885 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 886 infoList.add(info); 887 } 888 } 889 return infoList; 890 } 891 } 892 893 /** 894 * Return external input devices. 895 */ 896 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 897 synchronized (mLock) { 898 return mSafeExternalInputs; 899 } 900 } 901 902 @ServiceThreadOnly 903 private void updateSafeDeviceInfoList() { 904 assertRunOnServiceThread(); 905 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 906 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 907 synchronized (mLock) { 908 mSafeAllDeviceInfos = copiedDevices; 909 mSafeExternalInputs = externalInputs; 910 } 911 } 912 913 /** 914 * Return a list of external cec input (source) devices. 915 * 916 * <p>Note that this effectively excludes non-source devices like system audio, 917 * secondary TV. 918 */ 919 private List<HdmiCecDeviceInfo> getInputDevices() { 920 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 921 for (int i = 0; i < mDeviceInfos.size(); ++i) { 922 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 923 if (isLocalDeviceAddress(i)) { 924 continue; 925 } 926 if (info.isSourceType()) { 927 infoList.add(info); 928 } 929 } 930 return infoList; 931 } 932 933 @ServiceThreadOnly 934 private boolean isLocalDeviceAddress(int address) { 935 assertRunOnServiceThread(); 936 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 937 if (device.isAddressOf(address)) { 938 return true; 939 } 940 } 941 return false; 942 } 943 944 @ServiceThreadOnly 945 HdmiCecDeviceInfo getAvrDeviceInfo() { 946 assertRunOnServiceThread(); 947 return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 948 } 949 950 /** 951 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 952 * 953 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 954 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 955 * 956 * @param logicalAddress logical address to be retrieved 957 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 958 * Returns null if no logical address matched 959 */ 960 @ServiceThreadOnly 961 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 962 assertRunOnServiceThread(); 963 return mDeviceInfos.get(logicalAddress); 964 } 965 966 boolean hasSystemAudioDevice() { 967 return getSafeAvrDeviceInfo() != null; 968 } 969 970 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 971 return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 972 } 973 974 /** 975 * Thread safe version of {@link #getDeviceInfo(int)}. 976 * 977 * @param logicalAddress logical address to be retrieved 978 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 979 * Returns null if no logical address matched 980 */ 981 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 982 synchronized (mLock) { 983 return mSafeAllDeviceInfos.get(logicalAddress); 984 } 985 } 986 987 /** 988 * Called when a device is newly added or a new device is detected or 989 * existing device is updated. 990 * 991 * @param info device info of a new device. 992 */ 993 @ServiceThreadOnly 994 final void addCecDevice(HdmiCecDeviceInfo info) { 995 assertRunOnServiceThread(); 996 addDeviceInfo(info); 997 if (info.getLogicalAddress() == mAddress) { 998 // The addition of TV device itself should not be notified. 999 return; 1000 } 1001 mService.invokeDeviceEventListeners(info, true); 1002 } 1003 1004 /** 1005 * Called when a device is removed or removal of device is detected. 1006 * 1007 * @param address a logical address of a device to be removed 1008 */ 1009 @ServiceThreadOnly 1010 final void removeCecDevice(int address) { 1011 assertRunOnServiceThread(); 1012 HdmiCecDeviceInfo info = removeDeviceInfo(address); 1013 1014 mCecMessageCache.flushMessagesFrom(address); 1015 mService.invokeDeviceEventListeners(info, false); 1016 } 1017 1018 @ServiceThreadOnly 1019 void handleRemoveActiveRoutingPath(int path) { 1020 assertRunOnServiceThread(); 1021 // Seq #23 1022 if (isTailOfActivePath(path, getActivePath())) { 1023 removeAction(RoutingControlAction.class); 1024 int newPath = mService.portIdToPath(getActivePortId()); 1025 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 1026 mAddress, getActivePath(), newPath)); 1027 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 1028 } 1029 } 1030 1031 /** 1032 * Launch routing control process. 1033 * 1034 * @param routingForBootup true if routing control is initiated due to One Touch Play 1035 * or TV power on 1036 */ 1037 @ServiceThreadOnly 1038 void launchRoutingControl(boolean routingForBootup) { 1039 assertRunOnServiceThread(); 1040 // Seq #24 1041 if (getActivePortId() != Constants.INVALID_PORT_ID) { 1042 if (!routingForBootup && !isProhibitMode()) { 1043 removeAction(RoutingControlAction.class); 1044 int newPath = mService.portIdToPath(getActivePortId()); 1045 setActivePath(newPath); 1046 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 1047 getActivePath(), newPath)); 1048 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 1049 routingForBootup, null)); 1050 } 1051 } else { 1052 int activePath = mService.getPhysicalAddress(); 1053 setActivePath(activePath); 1054 if (!routingForBootup) { 1055 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1056 activePath)); 1057 } 1058 } 1059 } 1060 1061 /** 1062 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 1063 * the given routing path. CEC devices use routing path for its physical address to 1064 * describe the hierarchy of the devices in the network. 1065 * 1066 * @param path routing path or physical address 1067 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 1068 */ 1069 @ServiceThreadOnly 1070 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 1071 assertRunOnServiceThread(); 1072 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 1073 if (info.getPhysicalAddress() == path) { 1074 return info; 1075 } 1076 } 1077 return null; 1078 } 1079 1080 /** 1081 * Whether a device of the specified physical address and logical address exists 1082 * in a device info list. However, both are minimal condition and it could 1083 * be different device from the original one. 1084 * 1085 * @param logicalAddress logical address of a device to be searched 1086 * @param physicalAddress physical address of a device to be searched 1087 * @return true if exist; otherwise false 1088 */ 1089 @ServiceThreadOnly 1090 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1091 assertRunOnServiceThread(); 1092 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1093 if (device == null) { 1094 return false; 1095 } 1096 return device.getPhysicalAddress() == physicalAddress; 1097 } 1098 1099 @Override 1100 @ServiceThreadOnly 1101 void onHotplug(int portId, boolean connected) { 1102 assertRunOnServiceThread(); 1103 1104 // Tv device will have permanent HotplugDetectionAction. 1105 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1106 if (!hotplugActions.isEmpty()) { 1107 // Note that hotplug action is single action running on a machine. 1108 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1109 // It covers seq #40, #43. 1110 hotplugActions.get(0).pollAllDevicesNow(); 1111 } 1112 } 1113 1114 @ServiceThreadOnly 1115 void setAutoDeviceOff(boolean enabled) { 1116 assertRunOnServiceThread(); 1117 mAutoDeviceOff = enabled; 1118 } 1119 1120 @Override 1121 @ServiceThreadOnly 1122 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1123 super.disableDevice(initiatedByCec, callback); 1124 assertRunOnServiceThread(); 1125 // Remove any repeated working actions. 1126 // HotplugDetectionAction will be reinstated during the wake up process. 1127 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1128 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1129 removeAction(DeviceDiscoveryAction.class); 1130 removeAction(HotplugDetectionAction.class); 1131 1132 disableSystemAudioIfExist(); 1133 disableArcIfExist(); 1134 checkIfPendingActionsCleared(); 1135 } 1136 1137 @ServiceThreadOnly 1138 private void disableSystemAudioIfExist() { 1139 assertRunOnServiceThread(); 1140 if (getAvrDeviceInfo() == null) { 1141 return; 1142 } 1143 1144 // Seq #31. 1145 removeAction(SystemAudioActionFromAvr.class); 1146 removeAction(SystemAudioActionFromTv.class); 1147 removeAction(SystemAudioAutoInitiationAction.class); 1148 removeAction(SystemAudioStatusAction.class); 1149 removeAction(VolumeControlAction.class); 1150 1151 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1152 // the system audio mode setting can be restored automatically. 1153 setSystemAudioMode(false, false); 1154 } 1155 1156 @ServiceThreadOnly 1157 private void disableArcIfExist() { 1158 assertRunOnServiceThread(); 1159 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 1160 if (avr == null) { 1161 return; 1162 } 1163 1164 // Seq #44. 1165 removeAction(RequestArcInitiationAction.class); 1166 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1167 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1168 } 1169 } 1170 1171 @Override 1172 @ServiceThreadOnly 1173 protected void onStandby(boolean initiatedByCec) { 1174 assertRunOnServiceThread(); 1175 // Seq #11 1176 if (!mService.isControlEnabled()) { 1177 return; 1178 } 1179 if (!initiatedByCec) { 1180 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1181 mAddress, Constants.ADDR_BROADCAST)); 1182 } 1183 } 1184 1185 @Override 1186 @ServiceThreadOnly 1187 protected boolean handleStandby(HdmiCecMessage message) { 1188 assertRunOnServiceThread(); 1189 // Seq #12 1190 // Tv accepts directly addressed <Standby> only. 1191 if (message.getDestination() == mAddress) { 1192 super.handleStandby(message); 1193 } 1194 return false; 1195 } 1196 1197 boolean isProhibitMode() { 1198 return mService.isProhibitMode(); 1199 } 1200 1201 boolean isPowerStandbyOrTransient() { 1202 return mService.isPowerStandbyOrTransient(); 1203 } 1204 1205 void displayOsd(int messageId) { 1206 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1207 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1208 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1209 HdmiControlService.PERMISSION); 1210 } 1211} 1212