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