HdmiCecLocalDeviceTv.java revision 6102bac706d351ce52ce385745f30aecaf835a2a
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(info, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 1336 } 1337 } 1338 1339 /** 1340 * Called when a device is removed or removal of device is detected. 1341 * 1342 * @param address a logical address of a device to be removed 1343 */ 1344 @ServiceThreadOnly 1345 final void removeCecDevice(int address) { 1346 assertRunOnServiceThread(); 1347 HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); 1348 1349 mCecMessageCache.flushMessagesFrom(address); 1350 invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 1351 } 1352 1353 @ServiceThreadOnly 1354 void handleRemoveActiveRoutingPath(int path) { 1355 assertRunOnServiceThread(); 1356 // Seq #23 1357 if (isTailOfActivePath(path, getActivePath())) { 1358 int newPath = mService.portIdToPath(getActivePortId()); 1359 startRoutingControl(getActivePath(), newPath, true, null); 1360 } 1361 } 1362 1363 /** 1364 * Launch routing control process. 1365 * 1366 * @param routingForBootup true if routing control is initiated due to One Touch Play 1367 * or TV power on 1368 */ 1369 @ServiceThreadOnly 1370 void launchRoutingControl(boolean routingForBootup) { 1371 assertRunOnServiceThread(); 1372 // Seq #24 1373 if (getActivePortId() != Constants.INVALID_PORT_ID) { 1374 if (!routingForBootup && !isProhibitMode()) { 1375 int newPath = mService.portIdToPath(getActivePortId()); 1376 setActivePath(newPath); 1377 startRoutingControl(getActivePath(), newPath, routingForBootup, null); 1378 } 1379 } else { 1380 int activePath = mService.getPhysicalAddress(); 1381 setActivePath(activePath); 1382 if (!routingForBootup) { 1383 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1384 activePath)); 1385 } 1386 } 1387 } 1388 1389 /** 1390 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 1391 * the given routing path. CEC devices use routing path for its physical address to 1392 * describe the hierarchy of the devices in the network. 1393 * 1394 * @param path routing path or physical address 1395 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 1396 */ 1397 @ServiceThreadOnly 1398 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 1399 assertRunOnServiceThread(); 1400 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 1401 if (info.getPhysicalAddress() == path) { 1402 return info; 1403 } 1404 } 1405 return null; 1406 } 1407 1408 /** 1409 * Whether a device of the specified physical address and logical address exists 1410 * in a device info list. However, both are minimal condition and it could 1411 * be different device from the original one. 1412 * 1413 * @param logicalAddress logical address of a device to be searched 1414 * @param physicalAddress physical address of a device to be searched 1415 * @return true if exist; otherwise false 1416 */ 1417 @ServiceThreadOnly 1418 private boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1419 assertRunOnServiceThread(); 1420 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 1421 if (device == null) { 1422 return false; 1423 } 1424 return device.getPhysicalAddress() == physicalAddress; 1425 } 1426 1427 @Override 1428 @ServiceThreadOnly 1429 void onHotplug(int portId, boolean connected) { 1430 assertRunOnServiceThread(); 1431 1432 if (!connected) { 1433 removeCecSwitches(portId); 1434 } 1435 // Tv device will have permanent HotplugDetectionAction. 1436 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1437 if (!hotplugActions.isEmpty()) { 1438 // Note that hotplug action is single action running on a machine. 1439 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1440 // It covers seq #40, #43. 1441 hotplugActions.get(0).pollAllDevicesNow(); 1442 } 1443 } 1444 1445 private void removeCecSwitches(int portId) { 1446 Iterator<Integer> it = mCecSwitches.iterator(); 1447 while (!it.hasNext()) { 1448 int path = it.next(); 1449 if (pathToPortId(path) == portId) { 1450 it.remove(); 1451 } 1452 } 1453 } 1454 1455 @ServiceThreadOnly 1456 void setAutoDeviceOff(boolean enabled) { 1457 assertRunOnServiceThread(); 1458 mAutoDeviceOff = enabled; 1459 } 1460 1461 @ServiceThreadOnly 1462 void setAutoWakeup(boolean enabled) { 1463 assertRunOnServiceThread(); 1464 mAutoWakeup = enabled; 1465 } 1466 1467 @ServiceThreadOnly 1468 boolean getAutoWakeup() { 1469 assertRunOnServiceThread(); 1470 return mAutoWakeup; 1471 } 1472 1473 @Override 1474 @ServiceThreadOnly 1475 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1476 super.disableDevice(initiatedByCec, callback); 1477 assertRunOnServiceThread(); 1478 mService.unregisterTvInputCallback(mTvInputCallback); 1479 // Remove any repeated working actions. 1480 // HotplugDetectionAction will be reinstated during the wake up process. 1481 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1482 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1483 removeAction(DeviceDiscoveryAction.class); 1484 removeAction(HotplugDetectionAction.class); 1485 removeAction(PowerStatusMonitorAction.class); 1486 // Remove recording actions. 1487 removeAction(OneTouchRecordAction.class); 1488 removeAction(TimerRecordingAction.class); 1489 1490 disableSystemAudioIfExist(); 1491 disableArcIfExist(); 1492 clearDeviceInfoList(); 1493 checkIfPendingActionsCleared(); 1494 } 1495 1496 @ServiceThreadOnly 1497 private void disableSystemAudioIfExist() { 1498 assertRunOnServiceThread(); 1499 if (getAvrDeviceInfo() == null) { 1500 return; 1501 } 1502 1503 // Seq #31. 1504 removeAction(SystemAudioActionFromAvr.class); 1505 removeAction(SystemAudioActionFromTv.class); 1506 removeAction(SystemAudioAutoInitiationAction.class); 1507 removeAction(SystemAudioStatusAction.class); 1508 removeAction(VolumeControlAction.class); 1509 1510 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1511 // the system audio mode setting can be restored automatically. 1512 setSystemAudioMode(false, false); 1513 } 1514 1515 @ServiceThreadOnly 1516 private void disableArcIfExist() { 1517 assertRunOnServiceThread(); 1518 HdmiDeviceInfo avr = getAvrDeviceInfo(); 1519 if (avr == null) { 1520 return; 1521 } 1522 1523 // Seq #44. 1524 removeAction(RequestArcInitiationAction.class); 1525 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1526 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1527 } 1528 } 1529 1530 @Override 1531 @ServiceThreadOnly 1532 protected void onStandby(boolean initiatedByCec) { 1533 assertRunOnServiceThread(); 1534 // Seq #11 1535 if (!mService.isControlEnabled()) { 1536 return; 1537 } 1538 if (!initiatedByCec && mAutoDeviceOff) { 1539 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1540 mAddress, Constants.ADDR_BROADCAST)); 1541 } 1542 } 1543 1544 boolean isProhibitMode() { 1545 return mService.isProhibitMode(); 1546 } 1547 1548 boolean isPowerStandbyOrTransient() { 1549 return mService.isPowerStandbyOrTransient(); 1550 } 1551 1552 @ServiceThreadOnly 1553 void displayOsd(int messageId) { 1554 assertRunOnServiceThread(); 1555 mService.displayOsd(messageId); 1556 } 1557 1558 @ServiceThreadOnly 1559 void displayOsd(int messageId, int extra) { 1560 assertRunOnServiceThread(); 1561 mService.displayOsd(messageId, extra); 1562 } 1563 1564 // Seq #54 and #55 1565 @ServiceThreadOnly 1566 int startOneTouchRecord(int recorderAddress, byte[] recordSource) { 1567 assertRunOnServiceThread(); 1568 if (!mService.isControlEnabled()) { 1569 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1570 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1571 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1572 } 1573 1574 if (!checkRecorder(recorderAddress)) { 1575 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1576 announceOneTouchRecordResult(recorderAddress, 1577 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1578 return Constants.ABORT_NOT_IN_CORRECT_MODE; 1579 } 1580 1581 if (!checkRecordSource(recordSource)) { 1582 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1583 announceOneTouchRecordResult(recorderAddress, 1584 ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN); 1585 return Constants.ABORT_UNABLE_TO_DETERMINE; 1586 } 1587 1588 addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource)); 1589 Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:" 1590 + Arrays.toString(recordSource)); 1591 return Constants.ABORT_NO_ERROR; 1592 } 1593 1594 @ServiceThreadOnly 1595 void stopOneTouchRecord(int recorderAddress) { 1596 assertRunOnServiceThread(); 1597 if (!mService.isControlEnabled()) { 1598 Slog.w(TAG, "Can not stop one touch record. CEC control is disabled."); 1599 announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED); 1600 return; 1601 } 1602 1603 if (!checkRecorder(recorderAddress)) { 1604 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1605 announceOneTouchRecordResult(recorderAddress, 1606 ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION); 1607 return; 1608 } 1609 1610 // Remove one touch record action so that other one touch record can be started. 1611 removeAction(OneTouchRecordAction.class); 1612 mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress)); 1613 Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress); 1614 } 1615 1616 private boolean checkRecorder(int recorderAddress) { 1617 HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress); 1618 return (device != null) 1619 && (HdmiUtils.getTypeFromAddress(recorderAddress) 1620 == HdmiDeviceInfo.DEVICE_RECORDER); 1621 } 1622 1623 private boolean checkRecordSource(byte[] recordSource) { 1624 return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource); 1625 } 1626 1627 @ServiceThreadOnly 1628 void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1629 assertRunOnServiceThread(); 1630 if (!mService.isControlEnabled()) { 1631 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1632 announceTimerRecordingResult(recorderAddress, 1633 TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED); 1634 return; 1635 } 1636 1637 if (!checkRecorder(recorderAddress)) { 1638 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1639 announceTimerRecordingResult(recorderAddress, 1640 TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION); 1641 return; 1642 } 1643 1644 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1645 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1646 announceTimerRecordingResult( 1647 recorderAddress, 1648 TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE); 1649 return; 1650 } 1651 1652 addAndStartAction( 1653 new TimerRecordingAction(this, recorderAddress, sourceType, recordSource)); 1654 Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:" 1655 + sourceType + ", RecordSource:" + Arrays.toString(recordSource)); 1656 } 1657 1658 private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) { 1659 return (recordSource != null) 1660 && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource); 1661 } 1662 1663 @ServiceThreadOnly 1664 void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1665 assertRunOnServiceThread(); 1666 if (!mService.isControlEnabled()) { 1667 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1668 announceClearTimerRecordingResult(recorderAddress, CLEAR_TIMER_STATUS_CEC_DISABLE); 1669 return; 1670 } 1671 1672 if (!checkRecorder(recorderAddress)) { 1673 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1674 announceClearTimerRecordingResult(recorderAddress, 1675 CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION); 1676 return; 1677 } 1678 1679 if (!checkTimerRecordingSource(sourceType, recordSource)) { 1680 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1681 announceClearTimerRecordingResult(recorderAddress, 1682 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1683 return; 1684 } 1685 1686 sendClearTimerMessage(recorderAddress, sourceType, recordSource); 1687 } 1688 1689 private void sendClearTimerMessage(final int recorderAddress, int sourceType, 1690 byte[] recordSource) { 1691 HdmiCecMessage message = null; 1692 switch (sourceType) { 1693 case TIMER_RECORDING_TYPE_DIGITAL: 1694 message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress, 1695 recordSource); 1696 break; 1697 case TIMER_RECORDING_TYPE_ANALOGUE: 1698 message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress, 1699 recordSource); 1700 break; 1701 case TIMER_RECORDING_TYPE_EXTERNAL: 1702 message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress, 1703 recordSource); 1704 break; 1705 default: 1706 Slog.w(TAG, "Invalid source type:" + recorderAddress); 1707 announceClearTimerRecordingResult(recorderAddress, 1708 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1709 return; 1710 1711 } 1712 mService.sendCecCommand(message, new SendMessageCallback() { 1713 @Override 1714 public void onSendCompleted(int error) { 1715 if (error != Constants.SEND_RESULT_SUCCESS) { 1716 announceClearTimerRecordingResult(recorderAddress, 1717 CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE); 1718 } 1719 } 1720 }); 1721 } 1722 1723 void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 1724 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 1725 if (info == null) { 1726 Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); 1727 return; 1728 } 1729 1730 if (info.getDevicePowerStatus() == newPowerStatus) { 1731 return; 1732 } 1733 1734 HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus); 1735 // addDeviceInfo replaces old device info with new one if exists. 1736 addDeviceInfo(newInfo); 1737 1738 invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 1739 } 1740 1741 @Override 1742 protected boolean handleMenuStatus(HdmiCecMessage message) { 1743 // Do nothing and just return true not to prevent from responding <Feature Abort>. 1744 return true; 1745 } 1746 1747 @Override 1748 protected void sendStandby(int deviceId) { 1749 HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId); 1750 if (targetDevice == null) { 1751 return; 1752 } 1753 int targetAddress = targetDevice.getLogicalAddress(); 1754 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress)); 1755 } 1756 1757 @ServiceThreadOnly 1758 void processAllDelayedMessages() { 1759 assertRunOnServiceThread(); 1760 mDelayedMessageBuffer.processAllMessages(); 1761 } 1762 1763 @ServiceThreadOnly 1764 void processDelayedMessages(int address) { 1765 assertRunOnServiceThread(); 1766 mDelayedMessageBuffer.processMessagesForDevice(address); 1767 } 1768 1769 @Override 1770 protected void dump(final IndentingPrintWriter pw) { 1771 super.dump(pw); 1772 pw.println("mArcEstablished: " + mArcEstablished); 1773 pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled); 1774 pw.println("mSystemAudioActivated: " + mSystemAudioActivated); 1775 pw.println("mSystemAudioMute: " + mSystemAudioMute); 1776 pw.println("mAutoDeviceOff: " + mAutoDeviceOff); 1777 pw.println("mAutoWakeup: " + mAutoWakeup); 1778 pw.println("mSkipRoutingControl: " + mSkipRoutingControl); 1779 pw.println("CEC devices:"); 1780 pw.increaseIndent(); 1781 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 1782 pw.println(info); 1783 } 1784 pw.decreaseIndent(); 1785 } 1786} 1787