HdmiCecLocalDeviceTv.java revision 4fcbf0b93e15e60eeb8b08a32d895de07c73cb1b
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()) { 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(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 HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message); 1041 mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED); 1042 return true; 1043 } 1044 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 1045 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 1046 addAndStartAction(action); 1047 return true; 1048 } 1049 1050 @Override 1051 @ServiceThreadOnly 1052 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 1053 assertRunOnServiceThread(); 1054 if (!isMessageForSystemAudio(message)) { 1055 HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message); 1056 // Ignore this message. 1057 return true; 1058 } 1059 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true); 1060 return true; 1061 } 1062 1063 // Seq #53 1064 @Override 1065 @ServiceThreadOnly 1066 protected boolean handleRecordTvScreen(HdmiCecMessage message) { 1067 List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class); 1068 if (!actions.isEmpty()) { 1069 // Assumes only one OneTouchRecordAction. 1070 OneTouchRecordAction action = actions.get(0); 1071 if (action.getRecorderAddress() != message.getSource()) { 1072 announceOneTouchRecordResult( 1073 message.getSource(), 1074 HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS); 1075 } 1076 return super.handleRecordTvScreen(message); 1077 } 1078 1079 int recorderAddress = message.getSource(); 1080 byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress); 1081 int reason = startOneTouchRecord(recorderAddress, recordSource); 1082 if (reason != Constants.ABORT_NO_ERROR) { 1083 mService.maySendFeatureAbortCommand(message, reason); 1084 } 1085 return true; 1086 } 1087 1088 @Override 1089 protected boolean handleTimerClearedStatus(HdmiCecMessage message) { 1090 byte[] params = message.getParams(); 1091 int timerClearedStatusData = params[0] & 0xFF; 1092 announceTimerRecordingResult(message.getSource(), timerClearedStatusData); 1093 return true; 1094 } 1095 1096 void announceOneTouchRecordResult(int recorderAddress, int result) { 1097 mService.invokeOneTouchRecordResult(recorderAddress, result); 1098 } 1099 1100 void announceTimerRecordingResult(int recorderAddress, int result) { 1101 mService.invokeTimerRecordingResult(recorderAddress, result); 1102 } 1103 1104 void announceClearTimerRecordingResult(int recorderAddress, int result) { 1105 mService.invokeClearTimerRecordingResult(recorderAddress, result); 1106 } 1107 1108 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 1109 return mService.isControlEnabled() 1110 && message.getSource() == Constants.ADDR_AUDIO_SYSTEM 1111 && (message.getDestination() == Constants.ADDR_TV 1112 || message.getDestination() == Constants.ADDR_BROADCAST) 1113 && getAvrDeviceInfo() != null; 1114 } 1115 1116 /** 1117 * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same 1118 * logical address as new device info's. 1119 * 1120 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1121 * 1122 * @param deviceInfo a new {@link HdmiDeviceInfo} to be added. 1123 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo} 1124 * that has the same logical address as new one has. 1125 */ 1126 @ServiceThreadOnly 1127 private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) { 1128 assertRunOnServiceThread(); 1129 HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress()); 1130 if (oldDeviceInfo != null) { 1131 removeDeviceInfo(deviceInfo.getId()); 1132 } 1133 mDeviceInfos.append(deviceInfo.getId(), deviceInfo); 1134 updateSafeDeviceInfoList(); 1135 return oldDeviceInfo; 1136 } 1137 1138 /** 1139 * Remove a device info corresponding to the given {@code logicalAddress}. 1140 * It returns removed {@link HdmiDeviceInfo} if exists. 1141 * 1142 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1143 * 1144 * @param id id of device to be removed 1145 * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null} 1146 */ 1147 @ServiceThreadOnly 1148 private HdmiDeviceInfo removeDeviceInfo(int id) { 1149 assertRunOnServiceThread(); 1150 HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id); 1151 if (deviceInfo != null) { 1152 mDeviceInfos.remove(id); 1153 } 1154 updateSafeDeviceInfoList(); 1155 return deviceInfo; 1156 } 1157 1158 /** 1159 * Return a list of all {@link HdmiDeviceInfo}. 1160 * 1161 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 1162 * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which 1163 * does not include local device. 1164 */ 1165 @ServiceThreadOnly 1166 List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) { 1167 assertRunOnServiceThread(); 1168 if (includeLocalDevice) { 1169 return HdmiUtils.sparseArrayToList(mDeviceInfos); 1170 } else { 1171 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1172 for (int i = 0; i < mDeviceInfos.size(); ++i) { 1173 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 1174 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 1175 infoList.add(info); 1176 } 1177 } 1178 return infoList; 1179 } 1180 } 1181 1182 /** 1183 * Return external input devices. 1184 */ 1185 List<HdmiDeviceInfo> getSafeExternalInputsLocked() { 1186 return mSafeExternalInputs; 1187 } 1188 1189 @ServiceThreadOnly 1190 private void updateSafeDeviceInfoList() { 1191 assertRunOnServiceThread(); 1192 List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 1193 List<HdmiDeviceInfo> externalInputs = getInputDevices(); 1194 synchronized (mLock) { 1195 mSafeAllDeviceInfos = copiedDevices; 1196 mSafeExternalInputs = externalInputs; 1197 } 1198 } 1199 1200 /** 1201 * Return a list of external cec input (source) devices. 1202 * 1203 * <p>Note that this effectively excludes non-source devices like system audio, 1204 * secondary TV. 1205 */ 1206 private List<HdmiDeviceInfo> getInputDevices() { 1207 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1208 for (int i = 0; i < mDeviceInfos.size(); ++i) { 1209 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 1210 if (isLocalDeviceAddress(info.getLogicalAddress())) { 1211 continue; 1212 } 1213 if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) { 1214 infoList.add(info); 1215 } 1216 } 1217 return infoList; 1218 } 1219 1220 // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch. 1221 // Returns true if the policy is set to true, and the device to check does not have 1222 // a parent CEC device (which should be the CEC-enabled switch) in the list. 1223 private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) { 1224 return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH 1225 && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches); 1226 } 1227 1228 private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) { 1229 for (int switchPath : switches) { 1230 if (isParentPath(switchPath, path)) { 1231 return true; 1232 } 1233 } 1234 return false; 1235 } 1236 1237 private static boolean isParentPath(int parentPath, int childPath) { 1238 // (A000, AB00) (AB00, ABC0), (ABC0, ABCD) 1239 // If child's last non-zero nibble is removed, the result equals to the parent. 1240 for (int i = 0; i <= 12; i += 4) { 1241 int nibble = (childPath >> i) & 0xF; 1242 if (nibble != 0) { 1243 int parentNibble = (parentPath >> i) & 0xF; 1244 return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4); 1245 } 1246 } 1247 return false; 1248 } 1249 1250 private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) { 1251 if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) { 1252 mService.invokeDeviceEventListeners(info, status); 1253 } 1254 } 1255 1256 private boolean isLocalDeviceAddress(int address) { 1257 return mLocalDeviceAddresses.contains(address); 1258 } 1259 1260 @ServiceThreadOnly 1261 HdmiDeviceInfo getAvrDeviceInfo() { 1262 assertRunOnServiceThread(); 1263 return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 1264 } 1265 1266 /** 1267 * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}. 1268 * 1269 * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}. 1270 * 1271 * @param logicalAddress logical address of the device to be retrieved 1272 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 1273 * Returns null if no logical address matched 1274 */ 1275 @ServiceThreadOnly 1276 HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) { 1277 assertRunOnServiceThread(); 1278 return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress)); 1279 } 1280 1281 boolean hasSystemAudioDevice() { 1282 return getSafeAvrDeviceInfo() != null; 1283 } 1284 1285 HdmiDeviceInfo getSafeAvrDeviceInfo() { 1286 return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 1287 } 1288 1289 /** 1290 * Thread safe version of {@link #getCecDeviceInfo(int)}. 1291 * 1292 * @param logicalAddress logical address to be retrieved 1293 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 1294 * Returns null if no logical address matched 1295 */ 1296 HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) { 1297 synchronized (mLock) { 1298 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1299 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) { 1300 return info; 1301 } 1302 } 1303 return null; 1304 } 1305 } 1306 1307 List<HdmiDeviceInfo> getSafeCecDevicesLocked() { 1308 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 1309 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1310 if (isLocalDeviceAddress(info.getLogicalAddress())) { 1311 continue; 1312 } 1313 infoList.add(info); 1314 } 1315 return infoList; 1316 } 1317 1318 /** 1319 * Called when a device is newly added or a new device is detected or 1320 * existing device is updated. 1321 * 1322 * @param info device info of a new device. 1323 */ 1324 @ServiceThreadOnly 1325 final void addCecDevice(HdmiDeviceInfo info) { 1326 assertRunOnServiceThread(); 1327 HdmiDeviceInfo old = addDeviceInfo(info); 1328 if (info.getLogicalAddress() == mAddress) { 1329 // The addition of TV device itself should not be notified. 1330 return; 1331 } 1332 if (old == null) { 1333 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 1334 } else if (!old.equals(info)) { 1335 invokeDeviceEventListener(old, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 1336 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 1337 } 1338 } 1339 1340 /** 1341 * Called when a device is removed or removal of device is detected. 1342 * 1343 * @param address a logical address of a device to be removed 1344 */ 1345 @ServiceThreadOnly 1346 final void removeCecDevice(int address) { 1347 assertRunOnServiceThread(); 1348 HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); 1349 1350 mCecMessageCache.flushMessagesFrom(address); 1351 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 1352 } 1353 1354 @ServiceThreadOnly 1355 void handleRemoveActiveRoutingPath(int path) { 1356 assertRunOnServiceThread(); 1357 // Seq #23 1358 if (isTailOfActivePath(path, getActivePath())) { 1359 int newPath = mService.portIdToPath(getActivePortId()); 1360 startRoutingControl(getActivePath(), newPath, true, null); 1361 } 1362 } 1363 1364 /** 1365 * Launch routing control process. 1366 * 1367 * @param routingForBootup true if routing control is initiated due to One Touch Play 1368 * or TV power on 1369 */ 1370 @ServiceThreadOnly 1371 void launchRoutingControl(boolean routingForBootup) { 1372 assertRunOnServiceThread(); 1373 // Seq #24 1374 if (getActivePortId() != Constants.INVALID_PORT_ID) { 1375 if (!routingForBootup && !isProhibitMode()) { 1376 int newPath = mService.portIdToPath(getActivePortId()); 1377 setActivePath(newPath); 1378 startRoutingControl(getActivePath(), newPath, routingForBootup, null); 1379 } 1380 } else { 1381 int activePath = mService.getPhysicalAddress(); 1382 setActivePath(activePath); 1383 if (!routingForBootup) { 1384 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1385 activePath)); 1386 } 1387 } 1388 } 1389 1390 /** 1391 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 1392 * the given routing path. CEC devices use routing path for its physical address to 1393 * describe the hierarchy of the devices in the network. 1394 * 1395 * @param path routing path or physical address 1396 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 1397 */ 1398 @ServiceThreadOnly 1399 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 1400 assertRunOnServiceThread(); 1401 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 1402 if (info.getPhysicalAddress() == path) { 1403 return info; 1404 } 1405 } 1406 return null; 1407 } 1408 1409 /** 1410 * Whether a device of the specified physical address and logical address exists 1411 * in a device info list. However, both are minimal condition and it could 1412 * be different device from the original one. 1413 * 1414 * @param logicalAddress logical address of a device to be searched 1415 * @param physicalAddress physical address of a device to be searched 1416 * @return true if exist; otherwise false 1417 */ 1418 @ServiceThreadOnly 1419 private boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1420 assertRunOnServiceThread(); 1421 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 1422 if (device == null) { 1423 return false; 1424 } 1425 return device.getPhysicalAddress() == physicalAddress; 1426 } 1427 1428 @Override 1429 @ServiceThreadOnly 1430 void onHotplug(int portId, boolean connected) { 1431 assertRunOnServiceThread(); 1432 1433 if (!connected) { 1434 removeCecSwitches(portId); 1435 } 1436 // Tv device will have permanent HotplugDetectionAction. 1437 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1438 if (!hotplugActions.isEmpty()) { 1439 // Note that hotplug action is single action running on a machine. 1440 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1441 // It covers seq #40, #43. 1442 hotplugActions.get(0).pollAllDevicesNow(); 1443 } 1444 } 1445 1446 private void removeCecSwitches(int portId) { 1447 Iterator<Integer> it = mCecSwitches.iterator(); 1448 while (!it.hasNext()) { 1449 int path = it.next(); 1450 if (pathToPortId(path) == portId) { 1451 it.remove(); 1452 } 1453 } 1454 } 1455 1456 @ServiceThreadOnly 1457 void setAutoDeviceOff(boolean enabled) { 1458 assertRunOnServiceThread(); 1459 mAutoDeviceOff = enabled; 1460 } 1461 1462 @ServiceThreadOnly 1463 void setAutoWakeup(boolean enabled) { 1464 assertRunOnServiceThread(); 1465 mAutoWakeup = enabled; 1466 } 1467 1468 @ServiceThreadOnly 1469 boolean getAutoWakeup() { 1470 assertRunOnServiceThread(); 1471 return mAutoWakeup; 1472 } 1473 1474 @Override 1475 @ServiceThreadOnly 1476 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1477 super.disableDevice(initiatedByCec, callback); 1478 assertRunOnServiceThread(); 1479 mService.unregisterTvInputCallback(mTvInputCallback); 1480 // Remove any repeated working actions. 1481 // HotplugDetectionAction will be reinstated during the wake up process. 1482 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1483 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1484 removeAction(DeviceDiscoveryAction.class); 1485 removeAction(HotplugDetectionAction.class); 1486 removeAction(PowerStatusMonitorAction.class); 1487 // Remove recording actions. 1488 removeAction(OneTouchRecordAction.class); 1489 removeAction(TimerRecordingAction.class); 1490 1491 disableSystemAudioIfExist(); 1492 disableArcIfExist(); 1493 clearDeviceInfoList(); 1494 checkIfPendingActionsCleared(); 1495 } 1496 1497 @ServiceThreadOnly 1498 private void disableSystemAudioIfExist() { 1499 assertRunOnServiceThread(); 1500 if (getAvrDeviceInfo() == null) { 1501 return; 1502 } 1503 1504 // Seq #31. 1505 removeAction(SystemAudioActionFromAvr.class); 1506 removeAction(SystemAudioActionFromTv.class); 1507 removeAction(SystemAudioAutoInitiationAction.class); 1508 removeAction(SystemAudioStatusAction.class); 1509 removeAction(VolumeControlAction.class); 1510 1511 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1512 // the system audio mode setting can be restored automatically. 1513 setSystemAudioMode(false, false); 1514 } 1515 1516 @ServiceThreadOnly 1517 private void disableArcIfExist() { 1518 assertRunOnServiceThread(); 1519 HdmiDeviceInfo avr = getAvrDeviceInfo(); 1520 if (avr == null) { 1521 return; 1522 } 1523 1524 // Seq #44. 1525 removeAction(RequestArcInitiationAction.class); 1526 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1527 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1528 } 1529 } 1530 1531 @Override 1532 @ServiceThreadOnly 1533 protected void onStandby(boolean initiatedByCec) { 1534 assertRunOnServiceThread(); 1535 // Seq #11 1536 if (!mService.isControlEnabled()) { 1537 return; 1538 } 1539 if (!initiatedByCec && mAutoDeviceOff) { 1540 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1541 mAddress, Constants.ADDR_BROADCAST)); 1542 } 1543 } 1544 1545 boolean isProhibitMode() { 1546 return mService.isProhibitMode(); 1547 } 1548 1549 boolean isPowerStandbyOrTransient() { 1550 return mService.isPowerStandbyOrTransient(); 1551 } 1552 1553 @ServiceThreadOnly 1554 void displayOsd(int messageId) { 1555 assertRunOnServiceThread(); 1556 mService.displayOsd(messageId); 1557 } 1558 1559 @ServiceThreadOnly 1560 void displayOsd(int messageId, int extra) { 1561 assertRunOnServiceThread(); 1562 mService.displayOsd(messageId, extra); 1563 } 1564 1565 // Seq #54 and #55 1566 @ServiceThreadOnly 1567 int startOneTouchRecord(int recorderAddress, byte[] recordSource) { 1568 assertRunOnServiceThread(); 1569 if (!mService.isControlEnabled()) { 1570 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1571 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1572 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1573 } 1574 1575 if (!checkRecorder(recorderAddress)) { 1576 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1577 announceOneTouchRecordResult(recorderAddress, 1578 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1579 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1580 } 1581 1582 if (!checkRecordSource(recordSource)) { 1583 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1584 announceOneTouchRecordResult(recorderAddress, 1585 ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN); 1586 return Constants.ABORT_UNABLE_TO_DETERMINE; 1587 } 1588 1589 addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource)); 1590 Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:" 1591 + Arrays.toString(recordSource)); 1592 return Constants.ABORT_NO_ERROR; 1593 } 1594 1595 @ServiceThreadOnly 1596 void stopOneTouchRecord(int recorderAddress) { 1597 assertRunOnServiceThread(); 1598 if (!mService.isControlEnabled()) { 1599 Slog.w(TAG, "Can not stop one touch record. CEC control is disabled."); 1600 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1601 return; 1602 } 1603 1604 if (!checkRecorder(recorderAddress)) { 1605 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1606 announceOneTouchRecordResult(recorderAddress, 1607 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1608 return; 1609 } 1610 1611 // Remove one touch record action so that other one touch record can be started. 1612 removeAction(OneTouchRecordAction.class); 1613 mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress)); 1614 Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress); 1615 } 1616 1617 private boolean checkRecorder(int recorderAddress) { 1618 HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress); 1619 return (device != null) 1620 && (HdmiUtils.getTypeFromAddress(recorderAddress) 1621 == HdmiDeviceInfo.DEVICE_RECORDER); 1622 } 1623 1624 private boolean checkRecordSource(byte[] recordSource) { 1625 return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource); 1626 } 1627 1628 @ServiceThreadOnly 1629 void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1630 assertRunOnServiceThread(); 1631 if (!mService.isControlEnabled()) { 1632 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1633 announceTimerRecordingResult(recorderAddress, 1634 TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED); 1635 return; 1636 } 1637 1638 if (!checkRecorder(recorderAddress)) { 1639 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1640 announceTimerRecordingResult(recorderAddress, 1641 TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION); 1642 return; 1643 } 1644 1645 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1646 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1647 announceTimerRecordingResult( 1648 recorderAddress, 1649 TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE); 1650 return; 1651 } 1652 1653 addAndStartAction( 1654 new TimerRecordingAction(this, recorderAddress, sourceType, recordSource)); 1655 Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:" 1656 + sourceType + ", RecordSource:" + Arrays.toString(recordSource)); 1657 } 1658 1659 private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) { 1660 return (recordSource != null) 1661 && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource); 1662 } 1663 1664 @ServiceThreadOnly 1665 void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1666 assertRunOnServiceThread(); 1667 if (!mService.isControlEnabled()) { 1668 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1669 announceClearTimerRecordingResult(recorderAddress, CLEAR_TIMER_STATUS_CEC_DISABLE); 1670 return; 1671 } 1672 1673 if (!checkRecorder(recorderAddress)) { 1674 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1675 announceClearTimerRecordingResult(recorderAddress, 1676 CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION); 1677 return; 1678 } 1679 1680 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1681 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1682 announceClearTimerRecordingResult(recorderAddress, 1683 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1684 return; 1685 } 1686 1687 sendClearTimerMessage(recorderAddress, sourceType, recordSource); 1688 } 1689 1690 private void sendClearTimerMessage(final int recorderAddress, int sourceType, 1691 byte[] recordSource) { 1692 HdmiCecMessage message = null; 1693 switch (sourceType) { 1694 case TIMER_RECORDING_TYPE_DIGITAL: 1695 message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress, 1696 recordSource); 1697 break; 1698 case TIMER_RECORDING_TYPE_ANALOGUE: 1699 message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress, 1700 recordSource); 1701 break; 1702 case TIMER_RECORDING_TYPE_EXTERNAL: 1703 message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress, 1704 recordSource); 1705 break; 1706 default: 1707 Slog.w(TAG, "Invalid source type:" + recorderAddress); 1708 announceClearTimerRecordingResult(recorderAddress, 1709 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1710 return; 1711 1712 } 1713 mService.sendCecCommand(message, new SendMessageCallback() { 1714 @Override 1715 public void onSendCompleted(int error) { 1716 if (error != Constants.SEND_RESULT_SUCCESS) { 1717 announceClearTimerRecordingResult(recorderAddress, 1718 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1719 } 1720 } 1721 }); 1722 } 1723 1724 void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 1725 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 1726 if (info == null) { 1727 Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); 1728 return; 1729 } 1730 1731 if (info.getDevicePowerStatus() == newPowerStatus) { 1732 return; 1733 } 1734 1735 HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus); 1736 // addDeviceInfo replaces old device info with new one if exists. 1737 addDeviceInfo(newInfo); 1738 1739 invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 1740 } 1741 1742 @Override 1743 protected boolean handleMenuStatus(HdmiCecMessage message) { 1744 // Do nothing and just return true not to prevent from responding <Feature Abort>. 1745 return true; 1746 } 1747 1748 @Override 1749 protected void sendStandby(int deviceId) { 1750 HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId); 1751 if (targetDevice == null) { 1752 return; 1753 } 1754 int targetAddress = targetDevice.getLogicalAddress(); 1755 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress)); 1756 } 1757 1758 @ServiceThreadOnly 1759 void processAllDelayedMessages() { 1760 assertRunOnServiceThread(); 1761 mDelayedMessageBuffer.processAllMessages(); 1762 } 1763 1764 @ServiceThreadOnly 1765 void processDelayedMessages(int address) { 1766 assertRunOnServiceThread(); 1767 mDelayedMessageBuffer.processMessagesForDevice(address); 1768 } 1769 1770 @Override 1771 protected void dump(final IndentingPrintWriter pw) { 1772 super.dump(pw); 1773 pw.println("mArcEstablished: " + mArcEstablished); 1774 pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled); 1775 pw.println("mSystemAudioActivated: " + mSystemAudioActivated); 1776 pw.println("mSystemAudioMute: " + mSystemAudioMute); 1777 pw.println("mAutoDeviceOff: " + mAutoDeviceOff); 1778 pw.println("mAutoWakeup: " + mAutoWakeup); 1779 pw.println("mSkipRoutingControl: " + mSkipRoutingControl); 1780 pw.println("CEC devices:"); 1781 pw.increaseIndent(); 1782 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1783 pw.println(info); 1784 } 1785 pw.decreaseIndent(); 1786 } 1787} 1788