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