HdmiCecLocalDeviceTv.java revision ad1e3d7df42376cd2a257b6c3b2fed540658a6e3
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 static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_CEC_DISABLE; 20import static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION; 21import static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE; 22import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_CEC_DISABLED; 23import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION; 24import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN; 25import static android.hardware.hdmi.HdmiControlManager.OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT; 26import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED; 27import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION; 28import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE; 29import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_ANALOGUE; 30import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_DIGITAL; 31import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_EXTERNAL; 32 33import android.annotation.Nullable; 34import android.content.Context; 35import android.hardware.hdmi.HdmiControlManager; 36import android.hardware.hdmi.HdmiDeviceInfo; 37import android.hardware.hdmi.HdmiRecordSources; 38import android.hardware.hdmi.HdmiTimerRecordSources; 39import android.hardware.hdmi.IHdmiControlCallback; 40import android.media.AudioManager; 41import android.media.AudioSystem; 42import android.media.tv.TvInputInfo; 43import android.media.tv.TvInputManager; 44import android.media.tv.TvInputManager.TvInputCallback; 45import android.os.RemoteException; 46import android.os.SystemProperties; 47import android.provider.Settings.Global; 48import android.util.ArraySet; 49import android.util.Slog; 50import android.util.SparseArray; 51 52import com.android.internal.annotations.GuardedBy; 53import com.android.internal.util.IndentingPrintWriter; 54import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 55import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 56import com.android.server.hdmi.HdmiControlService.SendMessageCallback; 57import com.android.server.SystemService; 58 59import java.io.UnsupportedEncodingException; 60import java.util.ArrayList; 61import java.util.Arrays; 62import java.util.Collection; 63import java.util.Collections; 64import java.util.Iterator; 65import java.util.List; 66 67/** 68 * Represent a logical device of type TV residing in Android system. 69 */ 70final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 71 private static final String TAG = "HdmiCecLocalDeviceTv"; 72 73 // Whether ARC is available or not. "true" means that ARC is established between TV and 74 // AVR as audio receiver. 75 @ServiceThreadOnly 76 private boolean mArcEstablished = false; 77 78 // Whether ARC feature is enabled or not. The default value is true. 79 // TODO: once adding system setting for it, read the value to it. 80 private boolean mArcFeatureEnabled = true; 81 82 // Whether System audio mode is activated or not. 83 // This becomes true only when all system audio sequences are finished. 84 @GuardedBy("mLock") 85 private boolean mSystemAudioActivated = false; 86 87 // The previous port id (input) before switching to the new one. This is remembered in order to 88 // be able to switch to it upon receiving <Inactive Source> from currently active source. 89 // This remains valid only when the active source was switched via one touch play operation 90 // (either by TV or source device). Manual port switching invalidates this value to 91 // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything. 92 @GuardedBy("mLock") 93 private int mPrevPortId; 94 95 @GuardedBy("mLock") 96 private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME; 97 98 @GuardedBy("mLock") 99 private boolean mSystemAudioMute = false; 100 101 // Copy of mDeviceInfos to guarantee thread-safety. 102 @GuardedBy("mLock") 103 private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 104 // All external cec input(source) devices. Does not include system audio device. 105 @GuardedBy("mLock") 106 private List<HdmiDeviceInfo> mSafeExternalInputs = Collections.emptyList(); 107 108 // Map-like container of all cec devices including local ones. 109 // device id is used as key of container. 110 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 111 private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>(); 112 113 // If true, TV going to standby mode puts other devices also to standby. 114 private boolean mAutoDeviceOff; 115 116 // If true, TV wakes itself up when receiving <Text/Image View On>. 117 private boolean mAutoWakeup; 118 119 // List of the logical address of local CEC devices. Unmodifiable, thread-safe. 120 private List<Integer> mLocalDeviceAddresses; 121 122 private final HdmiCecStandbyModeHandler mStandbyHandler; 123 124 // If true, do not do routing control/send active source for internal source. 125 // Set to true when the device was woken up by <Text/Image View On>. 126 private boolean mSkipRoutingControl; 127 128 // Set of physical addresses of CEC switches on the CEC bus. Managed independently from 129 // other CEC devices since they might not have logical address. 130 private final ArraySet<Integer> mCecSwitches = new ArraySet<Integer>(); 131 132 // Message buffer used to buffer selected messages to process later. <Active Source> 133 // from a source device, for instance, needs to be buffered if the device is not 134 // discovered yet. The buffered commands are taken out and when they are ready to 135 // handle. 136 private final DelayedMessageBuffer mDelayedMessageBuffer = new DelayedMessageBuffer(this); 137 138 // Defines the callback invoked when TV input framework is updated with input status. 139 // We are interested in the notification for HDMI input addition event, in order to 140 // process any CEC commands that arrived before the input is added. 141 private final TvInputCallback mTvInputCallback = new TvInputCallback() { 142 @Override 143 public void onInputAdded(String inputId) { 144 TvInputInfo tvInfo = mService.getTvInputManager().getTvInputInfo(inputId); 145 HdmiDeviceInfo info = tvInfo.getHdmiDeviceInfo(); 146 if (info != null && info.isCecDevice()) { 147 mDelayedMessageBuffer.processActiveSource(info.getLogicalAddress()); 148 } 149 } 150 }; 151 152 HdmiCecLocalDeviceTv(HdmiControlService service) { 153 super(service, HdmiDeviceInfo.DEVICE_TV); 154 mPrevPortId = Constants.INVALID_PORT_ID; 155 mAutoDeviceOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, 156 true); 157 mAutoWakeup = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, true); 158 mStandbyHandler = new HdmiCecStandbyModeHandler(service, this); 159 } 160 161 @Override 162 @ServiceThreadOnly 163 protected void onAddressAllocated(int logicalAddress, int reason) { 164 assertRunOnServiceThread(); 165 mService.registerTvInputCallback(mTvInputCallback); 166 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 167 mAddress, mService.getPhysicalAddress(), mDeviceType)); 168 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 169 mAddress, mService.getVendorId())); 170 mCecSwitches.add(mService.getPhysicalAddress()); // TV is a CEC switch too. 171 mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE); 172 launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC && 173 reason != HdmiControlService.INITIATED_BY_BOOT_UP); 174 mLocalDeviceAddresses = initLocalDeviceAddresses(); 175 launchDeviceDiscovery(); 176 startQueuedActions(); 177 } 178 179 180 @ServiceThreadOnly 181 private List<Integer> initLocalDeviceAddresses() { 182 assertRunOnServiceThread(); 183 List<Integer> addresses = new ArrayList<>(); 184 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 185 addresses.add(device.getDeviceInfo().getLogicalAddress()); 186 } 187 return Collections.unmodifiableList(addresses); 188 } 189 190 @Override 191 @ServiceThreadOnly 192 protected int getPreferredAddress() { 193 assertRunOnServiceThread(); 194 return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_TV, 195 Constants.ADDR_UNREGISTERED); 196 } 197 198 @Override 199 @ServiceThreadOnly 200 protected void setPreferredAddress(int addr) { 201 assertRunOnServiceThread(); 202 SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_TV, String.valueOf(addr)); 203 } 204 205 @Override 206 @ServiceThreadOnly 207 boolean dispatchMessage(HdmiCecMessage message) { 208 assertRunOnServiceThread(); 209 if (mService.isPowerStandby() && mStandbyHandler.handleCommand(message)) { 210 return true; 211 } 212 return super.onMessage(message); 213 } 214 215 /** 216 * Performs the action 'device select', or 'one touch play' initiated by TV. 217 * 218 * @param id id of HDMI device to select 219 * @param callback callback object to report the result with 220 */ 221 @ServiceThreadOnly 222 void deviceSelect(int id, IHdmiControlCallback callback) { 223 assertRunOnServiceThread(); 224 HdmiDeviceInfo targetDevice = mDeviceInfos.get(id); 225 if (targetDevice == null) { 226 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 227 return; 228 } 229 int targetAddress = targetDevice.getLogicalAddress(); 230 ActiveSource active = getActiveSource(); 231 if (active.isValid() && targetAddress == active.logicalAddress) { 232 invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); 233 return; 234 } 235 if (targetAddress == Constants.ADDR_INTERNAL) { 236 handleSelectInternalSource(); 237 // Switching to internal source is always successful even when CEC control is disabled. 238 setActiveSource(targetAddress, mService.getPhysicalAddress()); 239 setActivePath(mService.getPhysicalAddress()); 240 invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); 241 return; 242 } 243 if (!mService.isControlEnabled()) { 244 setActiveSource(targetDevice); 245 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 246 return; 247 } 248 removeAction(DeviceSelectAction.class); 249 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 250 } 251 252 @ServiceThreadOnly 253 private void handleSelectInternalSource() { 254 assertRunOnServiceThread(); 255 // Seq #18 256 if (mService.isControlEnabled() && mActiveSource.logicalAddress != mAddress) { 257 updateActiveSource(mAddress, mService.getPhysicalAddress()); 258 if (mSkipRoutingControl) { 259 mSkipRoutingControl = false; 260 return; 261 } 262 HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource( 263 mAddress, mService.getPhysicalAddress()); 264 mService.sendCecCommand(activeSource); 265 } 266 } 267 268 @ServiceThreadOnly 269 void updateActiveSource(int logicalAddress, int physicalAddress) { 270 assertRunOnServiceThread(); 271 updateActiveSource(ActiveSource.of(logicalAddress, physicalAddress)); 272 } 273 274 @ServiceThreadOnly 275 void updateActiveSource(ActiveSource newActive) { 276 assertRunOnServiceThread(); 277 // Seq #14 278 if (mActiveSource.equals(newActive)) { 279 return; 280 } 281 setActiveSource(newActive); 282 int logicalAddress = newActive.logicalAddress; 283 if (getCecDeviceInfo(logicalAddress) != null && logicalAddress != mAddress) { 284 if (mService.pathToPortId(newActive.physicalAddress) == getActivePortId()) { 285 setPrevPortId(getActivePortId()); 286 } 287 // TODO: Show the OSD banner related to the new active source device. 288 } else { 289 // TODO: If displayed, remove the OSD banner related to the previous 290 // active source device. 291 } 292 } 293 294 int getPortId(int physicalAddress) { 295 return mService.pathToPortId(physicalAddress); 296 } 297 298 /** 299 * Returns the previous port id kept to handle input switching on <Inactive Source>. 300 */ 301 int getPrevPortId() { 302 synchronized (mLock) { 303 return mPrevPortId; 304 } 305 } 306 307 /** 308 * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be 309 * taken for <Inactive Source>. 310 */ 311 void setPrevPortId(int portId) { 312 synchronized (mLock) { 313 mPrevPortId = portId; 314 } 315 } 316 317 @ServiceThreadOnly 318 void updateActiveInput(int path, boolean notifyInputChange) { 319 assertRunOnServiceThread(); 320 // Seq #15 321 setPrevPortId(getActivePortId()); 322 setActivePath(path); 323 // TODO: Handle PAP/PIP case. 324 // Show OSD port change banner 325 if (notifyInputChange) { 326 ActiveSource activeSource = getActiveSource(); 327 HdmiDeviceInfo info = getCecDeviceInfo(activeSource.logicalAddress); 328 if (info == null) { 329 info = new HdmiDeviceInfo(Constants.ADDR_INVALID, path, getActivePortId(), 330 HdmiDeviceInfo.DEVICE_RESERVED, 0, null); 331 } 332 mService.invokeInputChangeListener(info); 333 } 334 } 335 336 @ServiceThreadOnly 337 void doManualPortSwitching(int portId, IHdmiControlCallback callback) { 338 assertRunOnServiceThread(); 339 // Seq #20 340 if (!mService.isValidPortId(portId)) { 341 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 342 return; 343 } 344 if (portId == getActivePortId()) { 345 invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); 346 return; 347 } 348 mActiveSource.invalidate(); 349 if (!mService.isControlEnabled()) { 350 setActivePortId(portId); 351 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 352 return; 353 } 354 int oldPath = getActivePortId() != Constants.INVALID_PORT_ID 355 ? mService.portIdToPath(getActivePortId()) : getDeviceInfo().getPhysicalAddress(); 356 setActivePath(oldPath); 357 if (mSkipRoutingControl) { 358 mSkipRoutingControl = false; 359 return; 360 } 361 int newPath = mService.portIdToPath(portId); 362 startRoutingControl(oldPath, newPath, true, callback); 363 } 364 365 @ServiceThreadOnly 366 void startRoutingControl(int oldPath, int newPath, boolean queryDevicePowerStatus, 367 IHdmiControlCallback callback) { 368 assertRunOnServiceThread(); 369 if (oldPath == newPath) { 370 return; 371 } 372 HdmiCecMessage routingChange = 373 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 374 mService.sendCecCommand(routingChange); 375 removeAction(RoutingControlAction.class); 376 addAndStartAction( 377 new RoutingControlAction(this, newPath, queryDevicePowerStatus, callback)); 378 } 379 380 @ServiceThreadOnly 381 int getPowerStatus() { 382 assertRunOnServiceThread(); 383 return mService.getPowerStatus(); 384 } 385 386 /** 387 * Sends key to a target CEC device. 388 * 389 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 390 * @param isPressed true if this is key press event 391 */ 392 @Override 393 @ServiceThreadOnly 394 protected void sendKeyEvent(int keyCode, boolean isPressed) { 395 assertRunOnServiceThread(); 396 if (!HdmiCecKeycode.isSupportedKeycode(keyCode)) { 397 Slog.w(TAG, "Unsupported key: " + keyCode); 398 return; 399 } 400 List<SendKeyAction> action = getActions(SendKeyAction.class); 401 if (!action.isEmpty()) { 402 action.get(0).processKeyEvent(keyCode, isPressed); 403 } else { 404 if (isPressed) { 405 int logicalAddress = findKeyReceiverAddress(); 406 if (logicalAddress != Constants.ADDR_INVALID) { 407 addAndStartAction(new SendKeyAction(this, logicalAddress, keyCode)); 408 return; 409 } 410 } 411 Slog.w(TAG, "Discard key event: " + keyCode + " pressed:" + isPressed); 412 } 413 } 414 415 private int findKeyReceiverAddress() { 416 if (getActiveSource().isValid()) { 417 return getActiveSource().logicalAddress; 418 } 419 HdmiDeviceInfo info = getDeviceInfoByPath(getActivePath()); 420 if (info != null) { 421 return info.getLogicalAddress(); 422 } 423 return Constants.ADDR_INVALID; 424 } 425 426 private static void invokeCallback(IHdmiControlCallback callback, int result) { 427 if (callback == null) { 428 return; 429 } 430 try { 431 callback.onComplete(result); 432 } catch (RemoteException e) { 433 Slog.e(TAG, "Invoking callback failed:" + e); 434 } 435 } 436 437 @Override 438 @ServiceThreadOnly 439 protected boolean handleActiveSource(HdmiCecMessage message) { 440 assertRunOnServiceThread(); 441 int logicalAddress = message.getSource(); 442 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 443 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 444 if (info == null) { 445 if (!handleNewDeviceAtTheTailOfActivePath(physicalAddress)) { 446 mDelayedMessageBuffer.add(message); 447 } 448 } else { 449 ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress); 450 ActiveSourceHandler.create(this, null).process(activeSource, info.getDeviceType()); 451 } 452 return true; 453 } 454 455 @Override 456 @ServiceThreadOnly 457 protected boolean handleInactiveSource(HdmiCecMessage message) { 458 assertRunOnServiceThread(); 459 // Seq #10 460 461 // Ignore <Inactive Source> from non-active source device. 462 if (getActiveSource().logicalAddress != message.getSource()) { 463 return true; 464 } 465 if (isProhibitMode()) { 466 return true; 467 } 468 int portId = getPrevPortId(); 469 if (portId != Constants.INVALID_PORT_ID) { 470 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 471 472 HdmiDeviceInfo inactiveSource = getCecDeviceInfo(message.getSource()); 473 if (inactiveSource == null) { 474 return true; 475 } 476 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 477 return true; 478 } 479 // TODO: Switch the TV freeze mode off 480 481 doManualPortSwitching(portId, null); 482 setPrevPortId(Constants.INVALID_PORT_ID); 483 } 484 return true; 485 } 486 487 @Override 488 @ServiceThreadOnly 489 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 490 assertRunOnServiceThread(); 491 // Seq #19 492 if (mAddress == getActiveSource().logicalAddress) { 493 mService.sendCecCommand( 494 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 495 } 496 return true; 497 } 498 499 @Override 500 @ServiceThreadOnly 501 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 502 assertRunOnServiceThread(); 503 if (!broadcastMenuLanguage(mService.getLanguage())) { 504 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 505 } 506 return true; 507 } 508 509 @ServiceThreadOnly 510 boolean broadcastMenuLanguage(String language) { 511 assertRunOnServiceThread(); 512 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 513 mAddress, language); 514 if (command != null) { 515 mService.sendCecCommand(command); 516 return true; 517 } 518 return false; 519 } 520 521 @Override 522 @ServiceThreadOnly 523 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 524 assertRunOnServiceThread(); 525 int path = HdmiUtils.twoBytesToInt(message.getParams()); 526 int address = message.getSource(); 527 int type = message.getParams()[2]; 528 529 if (updateCecSwitchInfo(address, type, path)) return true; 530 531 // Ignore if [Device Discovery Action] is going on. 532 if (hasAction(DeviceDiscoveryAction.class)) { 533 Slog.i(TAG, "Ignored while Device Discovery Action is in progress: " + message); 534 return true; 535 } 536 537 if (!isInDeviceList(address, path)) { 538 handleNewDeviceAtTheTailOfActivePath(path); 539 } 540 startNewDeviceAction(ActiveSource.of(address, path), type); 541 return true; 542 } 543 544 @Override 545 protected boolean handleReportPowerStatus(HdmiCecMessage command) { 546 int newStatus = command.getParams()[0] & 0xFF; 547 updateDevicePowerStatus(command.getSource(), newStatus); 548 return true; 549 } 550 551 @Override 552 protected boolean handleTimerStatus(HdmiCecMessage message) { 553 // Do nothing. 554 return true; 555 } 556 557 @Override 558 protected boolean handleRecordStatus(HdmiCecMessage message) { 559 // Do nothing. 560 return true; 561 } 562 563 boolean updateCecSwitchInfo(int address, int type, int path) { 564 if (address == Constants.ADDR_UNREGISTERED 565 && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) { 566 mCecSwitches.add(path); 567 updateSafeDeviceInfoList(); 568 return true; // Pure switch does not need further processing. Return here. 569 } 570 if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) { 571 mCecSwitches.add(path); 572 } 573 return false; 574 } 575 576 void startNewDeviceAction(ActiveSource activeSource, int deviceType) { 577 for (NewDeviceAction action : getActions(NewDeviceAction.class)) { 578 // If there is new device action which has the same logical address and path 579 // ignore new request. 580 // NewDeviceAction is created whenever it receives <Report Physical Address>. 581 // And there is a chance starting NewDeviceAction for the same source. 582 // Usually, new device sends <Report Physical Address> when it's plugged 583 // in. However, TV can detect a new device from HotPlugDetectionAction, 584 // which sends <Give Physical Address> to the source for newly detected 585 // device. 586 if (action.isActionOf(activeSource)) { 587 return; 588 } 589 } 590 591 addAndStartAction(new NewDeviceAction(this, activeSource.logicalAddress, 592 activeSource.physicalAddress, deviceType)); 593 } 594 595 private boolean handleNewDeviceAtTheTailOfActivePath(int path) { 596 // Seq #22 597 if (isTailOfActivePath(path, getActivePath())) { 598 int newPath = mService.portIdToPath(getActivePortId()); 599 setActivePath(newPath); 600 startRoutingControl(getActivePath(), newPath, false, null); 601 return true; 602 } 603 return false; 604 } 605 606 /** 607 * Whether the given path is located in the tail of current active path. 608 * 609 * @param path to be tested 610 * @param activePath current active path 611 * @return true if the given path is located in the tail of current active path; otherwise, 612 * false 613 */ 614 static boolean isTailOfActivePath(int path, int activePath) { 615 // If active routing path is internal source, return false. 616 if (activePath == 0) { 617 return false; 618 } 619 for (int i = 12; i >= 0; i -= 4) { 620 int curActivePath = (activePath >> i) & 0xF; 621 if (curActivePath == 0) { 622 return true; 623 } else { 624 int curPath = (path >> i) & 0xF; 625 if (curPath != curActivePath) { 626 return false; 627 } 628 } 629 } 630 return false; 631 } 632 633 @Override 634 @ServiceThreadOnly 635 protected boolean handleRoutingChange(HdmiCecMessage message) { 636 assertRunOnServiceThread(); 637 // Seq #21 638 byte[] params = message.getParams(); 639 int currentPath = HdmiUtils.twoBytesToInt(params); 640 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 641 mActiveSource.invalidate(); 642 removeAction(RoutingControlAction.class); 643 int newPath = HdmiUtils.twoBytesToInt(params, 2); 644 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 645 } 646 return true; 647 } 648 649 @Override 650 @ServiceThreadOnly 651 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 652 assertRunOnServiceThread(); 653 654 byte params[] = message.getParams(); 655 int mute = params[0] & 0x80; 656 int volume = params[0] & 0x7F; 657 setAudioStatus(mute == 0x80, volume); 658 return true; 659 } 660 661 @Override 662 @ServiceThreadOnly 663 protected boolean handleTextViewOn(HdmiCecMessage message) { 664 assertRunOnServiceThread(); 665 if (mService.isPowerStandbyOrTransient() && mAutoWakeup) { 666 mService.wakeUp(); 667 } 668 return true; 669 } 670 671 @Override 672 @ServiceThreadOnly 673 protected boolean handleImageViewOn(HdmiCecMessage message) { 674 assertRunOnServiceThread(); 675 // Currently, it's the same as <Text View On>. 676 return handleTextViewOn(message); 677 } 678 679 @Override 680 @ServiceThreadOnly 681 protected boolean handleSetOsdName(HdmiCecMessage message) { 682 int source = message.getSource(); 683 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(source); 684 // If the device is not in device list, ignore it. 685 if (deviceInfo == null) { 686 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 687 return true; 688 } 689 String osdName = null; 690 try { 691 osdName = new String(message.getParams(), "US-ASCII"); 692 } catch (UnsupportedEncodingException e) { 693 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 694 return true; 695 } 696 697 if (deviceInfo.getDisplayName().equals(osdName)) { 698 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 699 return true; 700 } 701 702 addCecDevice(new HdmiDeviceInfo(deviceInfo.getLogicalAddress(), 703 deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(), 704 deviceInfo.getDeviceType(), deviceInfo.getVendorId(), osdName)); 705 return true; 706 } 707 708 @ServiceThreadOnly 709 private void launchDeviceDiscovery() { 710 assertRunOnServiceThread(); 711 clearDeviceInfoList(); 712 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 713 new DeviceDiscoveryCallback() { 714 @Override 715 public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) { 716 for (HdmiDeviceInfo info : deviceInfos) { 717 addCecDevice(info); 718 } 719 720 // Since we removed all devices when it's start and 721 // device discovery action does not poll local devices, 722 // we should put device info of local device manually here 723 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 724 addCecDevice(device.getDeviceInfo()); 725 } 726 727 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 728 addAndStartAction(new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this)); 729 730 // If there is AVR, initiate System Audio Auto initiation action, 731 // which turns on and off system audio according to last system 732 // audio setting. 733 HdmiDeviceInfo avr = getAvrDeviceInfo(); 734 if (avr != null) { 735 onNewAvrAdded(avr); 736 } 737 } 738 }); 739 addAndStartAction(action); 740 } 741 742 @ServiceThreadOnly 743 void onNewAvrAdded(HdmiDeviceInfo avr) { 744 assertRunOnServiceThread(); 745 if (getSystemAudioModeSetting() && !isSystemAudioActivated()) { 746 addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress())); 747 } 748 if (isArcFeatureEnabled() && !hasAction(SetArcTransmissionStateAction.class)) { 749 startArcAction(true); 750 } 751 } 752 753 // Clear all device info. 754 @ServiceThreadOnly 755 private void clearDeviceInfoList() { 756 assertRunOnServiceThread(); 757 for (HdmiDeviceInfo info : mSafeExternalInputs) { 758 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 759 } 760 mDeviceInfos.clear(); 761 updateSafeDeviceInfoList(); 762 } 763 764 @ServiceThreadOnly 765 // Seq #32 766 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 767 assertRunOnServiceThread(); 768 if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) { 769 setSystemAudioMode(false, true); 770 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 771 return; 772 } 773 HdmiDeviceInfo avr = getAvrDeviceInfo(); 774 if (avr == null) { 775 setSystemAudioMode(false, true); 776 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 777 return; 778 } 779 780 addAndStartAction( 781 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 782 } 783 784 // # Seq 25 785 void setSystemAudioMode(boolean on, boolean updateSetting) { 786 HdmiLogger.debug("System Audio Mode change[old:%b new:%b]", mSystemAudioActivated, on); 787 788 if (updateSetting) { 789 mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on); 790 } 791 updateAudioManagerForSystemAudio(on); 792 synchronized (mLock) { 793 if (mSystemAudioActivated != on) { 794 mSystemAudioActivated = on; 795 mService.announceSystemAudioModeChange(on); 796 } 797 } 798 } 799 800 private void updateAudioManagerForSystemAudio(boolean on) { 801 int device = mService.getAudioManager().setHdmiSystemAudioSupported(on); 802 HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device); 803 } 804 805 boolean isSystemAudioActivated() { 806 if (!hasSystemAudioDevice()) { 807 return false; 808 } 809 synchronized (mLock) { 810 return mSystemAudioActivated; 811 } 812 } 813 814 boolean getSystemAudioModeSetting() { 815 return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false); 816 } 817 818 /** 819 * Change ARC status into the given {@code enabled} status. 820 * 821 * @return {@code true} if ARC was in "Enabled" status 822 */ 823 @ServiceThreadOnly 824 boolean setArcStatus(boolean enabled) { 825 assertRunOnServiceThread(); 826 827 HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled); 828 boolean oldStatus = mArcEstablished; 829 // 1. Enable/disable ARC circuit. 830 mService.setAudioReturnChannel(getAvrDeviceInfo().getPortId(), enabled); 831 // 2. Notify arc status to audio service. 832 notifyArcStatusToAudioService(enabled); 833 // 3. Update arc status; 834 mArcEstablished = enabled; 835 return oldStatus; 836 } 837 838 private void notifyArcStatusToAudioService(boolean enabled) { 839 // Note that we don't set any name to ARC. 840 mService.getAudioManager().setWiredDeviceConnectionState( 841 AudioSystem.DEVICE_OUT_HDMI_ARC, 842 enabled ? 1 : 0, ""); 843 } 844 845 /** 846 * Returns whether ARC is enabled or not. 847 */ 848 @ServiceThreadOnly 849 boolean isArcEstabilished() { 850 assertRunOnServiceThread(); 851 return mArcFeatureEnabled && mArcEstablished; 852 } 853 854 @ServiceThreadOnly 855 void changeArcFeatureEnabled(boolean enabled) { 856 assertRunOnServiceThread(); 857 858 if (mArcFeatureEnabled != enabled) { 859 mArcFeatureEnabled = enabled; 860 if (enabled) { 861 if (!mArcEstablished) { 862 startArcAction(true); 863 } 864 } else { 865 if (mArcEstablished) { 866 startArcAction(false); 867 } 868 } 869 } 870 } 871 872 @ServiceThreadOnly 873 boolean isArcFeatureEnabled() { 874 assertRunOnServiceThread(); 875 return mArcFeatureEnabled; 876 } 877 878 @ServiceThreadOnly 879 void startArcAction(boolean enabled) { 880 assertRunOnServiceThread(); 881 HdmiDeviceInfo info = getAvrDeviceInfo(); 882 if (info == null) { 883 Slog.w(TAG, "Failed to start arc action; No AVR device."); 884 return; 885 } 886 if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) { 887 Slog.w(TAG, "Failed to start arc action; ARC configuration check failed."); 888 if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) { 889 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT); 890 } 891 return; 892 } 893 894 // Terminate opposite action and start action if not exist. 895 if (enabled) { 896 removeAction(RequestArcTerminationAction.class); 897 if (!hasAction(RequestArcInitiationAction.class)) { 898 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 899 } 900 } else { 901 removeAction(RequestArcInitiationAction.class); 902 if (!hasAction(RequestArcTerminationAction.class)) { 903 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 904 } 905 } 906 } 907 908 private boolean isDirectConnectAddress(int physicalAddress) { 909 return (physicalAddress & Constants.ROUTING_PATH_TOP_MASK) == physicalAddress; 910 } 911 912 void setAudioStatus(boolean mute, int volume) { 913 synchronized (mLock) { 914 mSystemAudioMute = mute; 915 mSystemAudioVolume = volume; 916 int maxVolume = mService.getAudioManager().getStreamMaxVolume( 917 AudioManager.STREAM_MUSIC); 918 mService.setAudioStatus(mute, 919 VolumeControlAction.scaleToCustomVolume(volume, maxVolume)); 920 displayOsd(HdmiControlManager.OSD_MESSAGE_AVR_VOLUME_CHANGED, 921 mute ? HdmiControlManager.AVR_VOLUME_MUTED : volume); 922 } 923 } 924 925 @ServiceThreadOnly 926 void changeVolume(int curVolume, int delta, int maxVolume) { 927 assertRunOnServiceThread(); 928 if (delta == 0 || !isSystemAudioActivated()) { 929 return; 930 } 931 932 int targetVolume = curVolume + delta; 933 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 934 synchronized (mLock) { 935 // If new volume is the same as current system audio volume, just ignore it. 936 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 937 if (cecVolume == mSystemAudioVolume) { 938 // Update tv volume with system volume value. 939 mService.setAudioStatus(false, 940 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 941 return; 942 } 943 } 944 945 List<VolumeControlAction> actions = getActions(VolumeControlAction.class); 946 if (actions.isEmpty()) { 947 addAndStartAction(new VolumeControlAction(this, 948 getAvrDeviceInfo().getLogicalAddress(), delta > 0)); 949 } else { 950 actions.get(0).handleVolumeChange(delta > 0); 951 } 952 } 953 954 @ServiceThreadOnly 955 void changeMute(boolean mute) { 956 assertRunOnServiceThread(); 957 HdmiLogger.debug("[A]:Change mute:%b", mute); 958 synchronized (mLock) { 959 if (mSystemAudioMute == mute) { 960 HdmiLogger.debug("No need to change mute."); 961 return; 962 } 963 } 964 if (!isSystemAudioActivated()) { 965 HdmiLogger.debug("[A]:System audio is not activated."); 966 return; 967 } 968 969 // Remove existing volume action. 970 removeAction(VolumeControlAction.class); 971 sendUserControlPressedAndReleased(getAvrDeviceInfo().getLogicalAddress(), 972 mute ? HdmiCecKeycode.CEC_KEYCODE_MUTE_FUNCTION : 973 HdmiCecKeycode.CEC_KEYCODE_RESTORE_VOLUME_FUNCTION); 974 } 975 976 @Override 977 @ServiceThreadOnly 978 protected boolean handleInitiateArc(HdmiCecMessage message) { 979 assertRunOnServiceThread(); 980 981 if (!canStartArcUpdateAction(message.getSource(), true)) { 982 if (getAvrDeviceInfo() == null) { 983 // AVR may not have been discovered yet. Delay the message processing. 984 mDelayedMessageBuffer.add(message); 985 return true; 986 } 987 mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED); 988 if (!isConnectedToArcPort(message.getSource())) { 989 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT); 990 } 991 return true; 992 } 993 994 // In case where <Initiate Arc> is started by <Request ARC Initiation> 995 // need to clean up RequestArcInitiationAction. 996 removeAction(RequestArcInitiationAction.class); 997 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 998 message.getSource(), true); 999 addAndStartAction(action); 1000 return true; 1001 } 1002 1003 private boolean canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled) { 1004 HdmiDeviceInfo avr = getAvrDeviceInfo(); 1005 if (avr != null 1006 && (avrAddress == avr.getLogicalAddress()) 1007 && isConnectedToArcPort(avr.getPhysicalAddress()) 1008 && isDirectConnectAddress(avr.getPhysicalAddress())) { 1009 if (shouldCheckArcFeatureEnabled) { 1010 return isArcFeatureEnabled(); 1011 } else { 1012 return true; 1013 } 1014 } else { 1015 return false; 1016 } 1017 } 1018 1019 @Override 1020 @ServiceThreadOnly 1021 protected boolean handleTerminateArc(HdmiCecMessage message) { 1022 assertRunOnServiceThread(); 1023 // In cast of termination, do not check ARC configuration in that AVR device 1024 // might be removed already. 1025 1026 // In case where <Terminate Arc> is started by <Request ARC Termination> 1027 // need to clean up RequestArcInitiationAction. 1028 removeAction(RequestArcTerminationAction.class); 1029 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 1030 message.getSource(), false); 1031 addAndStartAction(action); 1032 return true; 1033 } 1034 1035 @Override 1036 @ServiceThreadOnly 1037 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 1038 assertRunOnServiceThread(); 1039 if (!isMessageForSystemAudio(message)) { 1040 if (getAvrDeviceInfo() == null) { 1041 // AVR may not have been discovered yet. Delay the message processing. 1042 mDelayedMessageBuffer.add(message); 1043 return true; 1044 } 1045 HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message); 1046 mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED); 1047 return true; 1048 } 1049 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 1050 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 1051 addAndStartAction(action); 1052 return true; 1053 } 1054 1055 @Override 1056 @ServiceThreadOnly 1057 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 1058 assertRunOnServiceThread(); 1059 if (!isMessageForSystemAudio(message)) { 1060 HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message); 1061 // Ignore this message. 1062 return true; 1063 } 1064 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true); 1065 return true; 1066 } 1067 1068 // Seq #53 1069 @Override 1070 @ServiceThreadOnly 1071 protected boolean handleRecordTvScreen(HdmiCecMessage message) { 1072 List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class); 1073 if (!actions.isEmpty()) { 1074 // Assumes only one OneTouchRecordAction. 1075 OneTouchRecordAction action = actions.get(0); 1076 if (action.getRecorderAddress() != message.getSource()) { 1077 announceOneTouchRecordResult( 1078 message.getSource(), 1079 HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS); 1080 } 1081 return super.handleRecordTvScreen(message); 1082 } 1083 1084 int recorderAddress = message.getSource(); 1085 byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress); 1086 int reason = startOneTouchRecord(recorderAddress, recordSource); 1087 if (reason != Constants.ABORT_NO_ERROR) { 1088 mService.maySendFeatureAbortCommand(message, reason); 1089 } 1090 return true; 1091 } 1092 1093 @Override 1094 protected boolean handleTimerClearedStatus(HdmiCecMessage message) { 1095 byte[] params = message.getParams(); 1096 int timerClearedStatusData = params[0] & 0xFF; 1097 announceTimerRecordingResult(message.getSource(), timerClearedStatusData); 1098 return true; 1099 } 1100 1101 void announceOneTouchRecordResult(int recorderAddress, int result) { 1102 mService.invokeOneTouchRecordResult(recorderAddress, result); 1103 } 1104 1105 void announceTimerRecordingResult(int recorderAddress, int result) { 1106 mService.invokeTimerRecordingResult(recorderAddress, result); 1107 } 1108 1109 void announceClearTimerRecordingResult(int recorderAddress, int result) { 1110 mService.invokeClearTimerRecordingResult(recorderAddress, result); 1111 } 1112 1113 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 1114 return mService.isControlEnabled() 1115 && message.getSource() == Constants.ADDR_AUDIO_SYSTEM 1116 && (message.getDestination() == Constants.ADDR_TV 1117 || message.getDestination() == Constants.ADDR_BROADCAST) 1118 && getAvrDeviceInfo() != null; 1119 } 1120 1121 /** 1122 * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same 1123 * logical address as new device info's. 1124 * 1125 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1126 * 1127 * @param deviceInfo a new {@link HdmiDeviceInfo} to be added. 1128 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo} 1129 * that has the same logical address as new one has. 1130 */ 1131 @ServiceThreadOnly 1132 private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) { 1133 assertRunOnServiceThread(); 1134 HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress()); 1135 if (oldDeviceInfo != null) { 1136 removeDeviceInfo(deviceInfo.getId()); 1137 } 1138 mDeviceInfos.append(deviceInfo.getId(), deviceInfo); 1139 updateSafeDeviceInfoList(); 1140 return oldDeviceInfo; 1141 } 1142 1143 /** 1144 * Remove a device info corresponding to the given {@code logicalAddress}. 1145 * It returns removed {@link HdmiDeviceInfo} if exists. 1146 * 1147 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1148 * 1149 * @param id id of device to be removed 1150 * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null} 1151 */ 1152 @ServiceThreadOnly 1153 private HdmiDeviceInfo removeDeviceInfo(int id) { 1154 assertRunOnServiceThread(); 1155 HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id); 1156 if (deviceInfo != null) { 1157 mDeviceInfos.remove(id); 1158 } 1159 updateSafeDeviceInfoList(); 1160 return deviceInfo; 1161 } 1162 1163 /** 1164 * Return a list of all {@link HdmiDeviceInfo}. 1165 * 1166 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1167 * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which 1168 * does not include local device. 1169 */ 1170 @ServiceThreadOnly 1171 List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) { 1172 assertRunOnServiceThread(); 1173 if (includeLocalDevice) { 1174 return HdmiUtils.sparseArrayToList(mDeviceInfos); 1175 } else { 1176 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1177 for (int i = 0; i < mDeviceInfos.size(); ++i) { 1178 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 1179 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 1180 infoList.add(info); 1181 } 1182 } 1183 return infoList; 1184 } 1185 } 1186 1187 /** 1188 * Return external input devices. 1189 */ 1190 List<HdmiDeviceInfo> getSafeExternalInputsLocked() { 1191 return mSafeExternalInputs; 1192 } 1193 1194 @ServiceThreadOnly 1195 private void updateSafeDeviceInfoList() { 1196 assertRunOnServiceThread(); 1197 List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 1198 List<HdmiDeviceInfo> externalInputs = getInputDevices(); 1199 synchronized (mLock) { 1200 mSafeAllDeviceInfos = copiedDevices; 1201 mSafeExternalInputs = externalInputs; 1202 } 1203 } 1204 1205 /** 1206 * Return a list of external cec input (source) devices. 1207 * 1208 * <p>Note that this effectively excludes non-source devices like system audio, 1209 * secondary TV. 1210 */ 1211 private List<HdmiDeviceInfo> getInputDevices() { 1212 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1213 for (int i = 0; i < mDeviceInfos.size(); ++i) { 1214 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 1215 if (isLocalDeviceAddress(info.getLogicalAddress())) { 1216 continue; 1217 } 1218 if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) { 1219 infoList.add(info); 1220 } 1221 } 1222 return infoList; 1223 } 1224 1225 // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch. 1226 // Returns true if the policy is set to true, and the device to check does not have 1227 // a parent CEC device (which should be the CEC-enabled switch) in the list. 1228 private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) { 1229 return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH 1230 && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches); 1231 } 1232 1233 private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) { 1234 for (int switchPath : switches) { 1235 if (isParentPath(switchPath, path)) { 1236 return true; 1237 } 1238 } 1239 return false; 1240 } 1241 1242 private static boolean isParentPath(int parentPath, int childPath) { 1243 // (A000, AB00) (AB00, ABC0), (ABC0, ABCD) 1244 // If child's last non-zero nibble is removed, the result equals to the parent. 1245 for (int i = 0; i <= 12; i += 4) { 1246 int nibble = (childPath >> i) & 0xF; 1247 if (nibble != 0) { 1248 int parentNibble = (parentPath >> i) & 0xF; 1249 return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4); 1250 } 1251 } 1252 return false; 1253 } 1254 1255 private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) { 1256 if (!hideDevicesBehindLegacySwitch(info)) { 1257 mService.invokeDeviceEventListeners(info, status); 1258 } 1259 } 1260 1261 private boolean isLocalDeviceAddress(int address) { 1262 return mLocalDeviceAddresses.contains(address); 1263 } 1264 1265 @ServiceThreadOnly 1266 HdmiDeviceInfo getAvrDeviceInfo() { 1267 assertRunOnServiceThread(); 1268 return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 1269 } 1270 1271 /** 1272 * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}. 1273 * 1274 * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}. 1275 * 1276 * @param logicalAddress logical address of the device to be retrieved 1277 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 1278 * Returns null if no logical address matched 1279 */ 1280 @ServiceThreadOnly 1281 HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) { 1282 assertRunOnServiceThread(); 1283 return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress)); 1284 } 1285 1286 boolean hasSystemAudioDevice() { 1287 return getSafeAvrDeviceInfo() != null; 1288 } 1289 1290 HdmiDeviceInfo getSafeAvrDeviceInfo() { 1291 return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 1292 } 1293 1294 /** 1295 * Thread safe version of {@link #getCecDeviceInfo(int)}. 1296 * 1297 * @param logicalAddress logical address to be retrieved 1298 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 1299 * Returns null if no logical address matched 1300 */ 1301 HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) { 1302 synchronized (mLock) { 1303 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1304 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) { 1305 return info; 1306 } 1307 } 1308 return null; 1309 } 1310 } 1311 1312 List<HdmiDeviceInfo> getSafeCecDevicesLocked() { 1313 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1314 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1315 if (isLocalDeviceAddress(info.getLogicalAddress())) { 1316 continue; 1317 } 1318 infoList.add(info); 1319 } 1320 return infoList; 1321 } 1322 1323 /** 1324 * Called when a device is newly added or a new device is detected or 1325 * existing device is updated. 1326 * 1327 * @param info device info of a new device. 1328 */ 1329 @ServiceThreadOnly 1330 final void addCecDevice(HdmiDeviceInfo info) { 1331 assertRunOnServiceThread(); 1332 HdmiDeviceInfo old = addDeviceInfo(info); 1333 if (info.getLogicalAddress() == mAddress) { 1334 // The addition of TV device itself should not be notified. 1335 return; 1336 } 1337 if (old == null) { 1338 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 1339 } else if (!old.equals(info)) { 1340 invokeDeviceEventListener(old, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 1341 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 1342 } 1343 } 1344 1345 /** 1346 * Called when a device is removed or removal of device is detected. 1347 * 1348 * @param address a logical address of a device to be removed 1349 */ 1350 @ServiceThreadOnly 1351 final void removeCecDevice(int address) { 1352 assertRunOnServiceThread(); 1353 HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); 1354 1355 mCecMessageCache.flushMessagesFrom(address); 1356 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 1357 } 1358 1359 @ServiceThreadOnly 1360 void handleRemoveActiveRoutingPath(int path) { 1361 assertRunOnServiceThread(); 1362 // Seq #23 1363 if (isTailOfActivePath(path, getActivePath())) { 1364 int newPath = mService.portIdToPath(getActivePortId()); 1365 startRoutingControl(getActivePath(), newPath, true, null); 1366 } 1367 } 1368 1369 /** 1370 * Launch routing control process. 1371 * 1372 * @param routingForBootup true if routing control is initiated due to One Touch Play 1373 * or TV power on 1374 */ 1375 @ServiceThreadOnly 1376 void launchRoutingControl(boolean routingForBootup) { 1377 assertRunOnServiceThread(); 1378 // Seq #24 1379 if (getActivePortId() != Constants.INVALID_PORT_ID) { 1380 if (!routingForBootup && !isProhibitMode()) { 1381 int newPath = mService.portIdToPath(getActivePortId()); 1382 setActivePath(newPath); 1383 startRoutingControl(getActivePath(), newPath, routingForBootup, null); 1384 } 1385 } else { 1386 int activePath = mService.getPhysicalAddress(); 1387 setActivePath(activePath); 1388 if (!routingForBootup) { 1389 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1390 activePath)); 1391 } 1392 } 1393 } 1394 1395 /** 1396 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 1397 * the given routing path. CEC devices use routing path for its physical address to 1398 * describe the hierarchy of the devices in the network. 1399 * 1400 * @param path routing path or physical address 1401 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 1402 */ 1403 @ServiceThreadOnly 1404 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 1405 assertRunOnServiceThread(); 1406 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 1407 if (info.getPhysicalAddress() == path) { 1408 return info; 1409 } 1410 } 1411 return null; 1412 } 1413 1414 /** 1415 * Whether a device of the specified physical address and logical address exists 1416 * in a device info list. However, both are minimal condition and it could 1417 * be different device from the original one. 1418 * 1419 * @param logicalAddress logical address of a device to be searched 1420 * @param physicalAddress physical address of a device to be searched 1421 * @return true if exist; otherwise false 1422 */ 1423 @ServiceThreadOnly 1424 private boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1425 assertRunOnServiceThread(); 1426 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 1427 if (device == null) { 1428 return false; 1429 } 1430 return device.getPhysicalAddress() == physicalAddress; 1431 } 1432 1433 @Override 1434 @ServiceThreadOnly 1435 void onHotplug(int portId, boolean connected) { 1436 assertRunOnServiceThread(); 1437 1438 if (!connected) { 1439 removeCecSwitches(portId); 1440 } 1441 // Tv device will have permanent HotplugDetectionAction. 1442 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1443 if (!hotplugActions.isEmpty()) { 1444 // Note that hotplug action is single action running on a machine. 1445 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1446 // It covers seq #40, #43. 1447 hotplugActions.get(0).pollAllDevicesNow(); 1448 } 1449 } 1450 1451 private void removeCecSwitches(int portId) { 1452 Iterator<Integer> it = mCecSwitches.iterator(); 1453 while (!it.hasNext()) { 1454 int path = it.next(); 1455 if (pathToPortId(path) == portId) { 1456 it.remove(); 1457 } 1458 } 1459 } 1460 1461 @ServiceThreadOnly 1462 void setAutoDeviceOff(boolean enabled) { 1463 assertRunOnServiceThread(); 1464 mAutoDeviceOff = enabled; 1465 } 1466 1467 @ServiceThreadOnly 1468 void setAutoWakeup(boolean enabled) { 1469 assertRunOnServiceThread(); 1470 mAutoWakeup = enabled; 1471 } 1472 1473 @ServiceThreadOnly 1474 boolean getAutoWakeup() { 1475 assertRunOnServiceThread(); 1476 return mAutoWakeup; 1477 } 1478 1479 @Override 1480 @ServiceThreadOnly 1481 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1482 super.disableDevice(initiatedByCec, callback); 1483 assertRunOnServiceThread(); 1484 mService.unregisterTvInputCallback(mTvInputCallback); 1485 // Remove any repeated working actions. 1486 // HotplugDetectionAction will be reinstated during the wake up process. 1487 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1488 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1489 removeAction(DeviceDiscoveryAction.class); 1490 removeAction(HotplugDetectionAction.class); 1491 removeAction(PowerStatusMonitorAction.class); 1492 // Remove recording actions. 1493 removeAction(OneTouchRecordAction.class); 1494 removeAction(TimerRecordingAction.class); 1495 1496 disableSystemAudioIfExist(); 1497 disableArcIfExist(); 1498 clearDeviceInfoList(); 1499 checkIfPendingActionsCleared(); 1500 } 1501 1502 @ServiceThreadOnly 1503 private void disableSystemAudioIfExist() { 1504 assertRunOnServiceThread(); 1505 if (getAvrDeviceInfo() == null) { 1506 return; 1507 } 1508 1509 // Seq #31. 1510 removeAction(SystemAudioActionFromAvr.class); 1511 removeAction(SystemAudioActionFromTv.class); 1512 removeAction(SystemAudioAutoInitiationAction.class); 1513 removeAction(SystemAudioStatusAction.class); 1514 removeAction(VolumeControlAction.class); 1515 1516 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1517 // the system audio mode setting can be restored automatically. 1518 setSystemAudioMode(false, false); 1519 } 1520 1521 @ServiceThreadOnly 1522 private void disableArcIfExist() { 1523 assertRunOnServiceThread(); 1524 HdmiDeviceInfo avr = getAvrDeviceInfo(); 1525 if (avr == null) { 1526 return; 1527 } 1528 1529 // Seq #44. 1530 removeAction(RequestArcInitiationAction.class); 1531 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1532 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1533 } 1534 } 1535 1536 @Override 1537 @ServiceThreadOnly 1538 protected void onStandby(boolean initiatedByCec) { 1539 assertRunOnServiceThread(); 1540 // Seq #11 1541 if (!mService.isControlEnabled()) { 1542 return; 1543 } 1544 if (!initiatedByCec && mAutoDeviceOff) { 1545 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1546 mAddress, Constants.ADDR_BROADCAST)); 1547 } 1548 } 1549 1550 boolean isProhibitMode() { 1551 return mService.isProhibitMode(); 1552 } 1553 1554 boolean isPowerStandbyOrTransient() { 1555 return mService.isPowerStandbyOrTransient(); 1556 } 1557 1558 @ServiceThreadOnly 1559 void displayOsd(int messageId) { 1560 assertRunOnServiceThread(); 1561 mService.displayOsd(messageId); 1562 } 1563 1564 @ServiceThreadOnly 1565 void displayOsd(int messageId, int extra) { 1566 assertRunOnServiceThread(); 1567 mService.displayOsd(messageId, extra); 1568 } 1569 1570 // Seq #54 and #55 1571 @ServiceThreadOnly 1572 int startOneTouchRecord(int recorderAddress, byte[] recordSource) { 1573 assertRunOnServiceThread(); 1574 if (!mService.isControlEnabled()) { 1575 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1576 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1577 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1578 } 1579 1580 if (!checkRecorder(recorderAddress)) { 1581 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1582 announceOneTouchRecordResult(recorderAddress, 1583 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1584 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1585 } 1586 1587 if (!checkRecordSource(recordSource)) { 1588 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1589 announceOneTouchRecordResult(recorderAddress, 1590 ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN); 1591 return Constants.ABORT_UNABLE_TO_DETERMINE; 1592 } 1593 1594 addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource)); 1595 Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:" 1596 + Arrays.toString(recordSource)); 1597 return Constants.ABORT_NO_ERROR; 1598 } 1599 1600 @ServiceThreadOnly 1601 void stopOneTouchRecord(int recorderAddress) { 1602 assertRunOnServiceThread(); 1603 if (!mService.isControlEnabled()) { 1604 Slog.w(TAG, "Can not stop one touch record. CEC control is disabled."); 1605 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1606 return; 1607 } 1608 1609 if (!checkRecorder(recorderAddress)) { 1610 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1611 announceOneTouchRecordResult(recorderAddress, 1612 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1613 return; 1614 } 1615 1616 // Remove one touch record action so that other one touch record can be started. 1617 removeAction(OneTouchRecordAction.class); 1618 mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress)); 1619 Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress); 1620 } 1621 1622 private boolean checkRecorder(int recorderAddress) { 1623 HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress); 1624 return (device != null) 1625 && (HdmiUtils.getTypeFromAddress(recorderAddress) 1626 == HdmiDeviceInfo.DEVICE_RECORDER); 1627 } 1628 1629 private boolean checkRecordSource(byte[] recordSource) { 1630 return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource); 1631 } 1632 1633 @ServiceThreadOnly 1634 void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1635 assertRunOnServiceThread(); 1636 if (!mService.isControlEnabled()) { 1637 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1638 announceTimerRecordingResult(recorderAddress, 1639 TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED); 1640 return; 1641 } 1642 1643 if (!checkRecorder(recorderAddress)) { 1644 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1645 announceTimerRecordingResult(recorderAddress, 1646 TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION); 1647 return; 1648 } 1649 1650 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1651 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1652 announceTimerRecordingResult( 1653 recorderAddress, 1654 TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE); 1655 return; 1656 } 1657 1658 addAndStartAction( 1659 new TimerRecordingAction(this, recorderAddress, sourceType, recordSource)); 1660 Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:" 1661 + sourceType + ", RecordSource:" + Arrays.toString(recordSource)); 1662 } 1663 1664 private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) { 1665 return (recordSource != null) 1666 && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource); 1667 } 1668 1669 @ServiceThreadOnly 1670 void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1671 assertRunOnServiceThread(); 1672 if (!mService.isControlEnabled()) { 1673 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1674 announceClearTimerRecordingResult(recorderAddress, CLEAR_TIMER_STATUS_CEC_DISABLE); 1675 return; 1676 } 1677 1678 if (!checkRecorder(recorderAddress)) { 1679 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1680 announceClearTimerRecordingResult(recorderAddress, 1681 CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION); 1682 return; 1683 } 1684 1685 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1686 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1687 announceClearTimerRecordingResult(recorderAddress, 1688 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1689 return; 1690 } 1691 1692 sendClearTimerMessage(recorderAddress, sourceType, recordSource); 1693 } 1694 1695 private void sendClearTimerMessage(final int recorderAddress, int sourceType, 1696 byte[] recordSource) { 1697 HdmiCecMessage message = null; 1698 switch (sourceType) { 1699 case TIMER_RECORDING_TYPE_DIGITAL: 1700 message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress, 1701 recordSource); 1702 break; 1703 case TIMER_RECORDING_TYPE_ANALOGUE: 1704 message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress, 1705 recordSource); 1706 break; 1707 case TIMER_RECORDING_TYPE_EXTERNAL: 1708 message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress, 1709 recordSource); 1710 break; 1711 default: 1712 Slog.w(TAG, "Invalid source type:" + recorderAddress); 1713 announceClearTimerRecordingResult(recorderAddress, 1714 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1715 return; 1716 1717 } 1718 mService.sendCecCommand(message, new SendMessageCallback() { 1719 @Override 1720 public void onSendCompleted(int error) { 1721 if (error != Constants.SEND_RESULT_SUCCESS) { 1722 announceClearTimerRecordingResult(recorderAddress, 1723 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1724 } 1725 } 1726 }); 1727 } 1728 1729 void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 1730 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 1731 if (info == null) { 1732 Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); 1733 return; 1734 } 1735 1736 if (info.getDevicePowerStatus() == newPowerStatus) { 1737 return; 1738 } 1739 1740 HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus); 1741 // addDeviceInfo replaces old device info with new one if exists. 1742 addDeviceInfo(newInfo); 1743 1744 invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 1745 } 1746 1747 @Override 1748 protected boolean handleMenuStatus(HdmiCecMessage message) { 1749 // Do nothing and just return true not to prevent from responding <Feature Abort>. 1750 return true; 1751 } 1752 1753 @Override 1754 protected void sendStandby(int deviceId) { 1755 HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId); 1756 if (targetDevice == null) { 1757 return; 1758 } 1759 int targetAddress = targetDevice.getLogicalAddress(); 1760 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress)); 1761 } 1762 1763 @ServiceThreadOnly 1764 void processAllDelayedMessages() { 1765 assertRunOnServiceThread(); 1766 mDelayedMessageBuffer.processAllMessages(); 1767 } 1768 1769 @ServiceThreadOnly 1770 void processDelayedMessages(int address) { 1771 assertRunOnServiceThread(); 1772 mDelayedMessageBuffer.processMessagesForDevice(address); 1773 } 1774 1775 @Override 1776 protected void dump(final IndentingPrintWriter pw) { 1777 super.dump(pw); 1778 pw.println("mArcEstablished: " + mArcEstablished); 1779 pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled); 1780 pw.println("mSystemAudioActivated: " + mSystemAudioActivated); 1781 pw.println("mSystemAudioMute: " + mSystemAudioMute); 1782 pw.println("mAutoDeviceOff: " + mAutoDeviceOff); 1783 pw.println("mAutoWakeup: " + mAutoWakeup); 1784 pw.println("mSkipRoutingControl: " + mSkipRoutingControl); 1785 pw.println("CEC devices:"); 1786 pw.increaseIndent(); 1787 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1788 pw.println(info); 1789 } 1790 pw.decreaseIndent(); 1791 } 1792} 1793