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