HdmiCecLocalDeviceTv.java revision 75a77e7d6cbfc287c6126efd28b338b48b7ea70c
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 android.content.Intent; 20import android.hardware.hdmi.HdmiCecDeviceInfo; 21import android.hardware.hdmi.HdmiControlManager; 22import android.hardware.hdmi.IHdmiControlCallback; 23import android.media.AudioSystem; 24import android.os.RemoteException; 25import android.os.UserHandle; 26import android.util.Slog; 27import android.util.SparseArray; 28 29import com.android.internal.annotations.GuardedBy; 30import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 31import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 32 33import java.io.UnsupportedEncodingException; 34import java.util.ArrayList; 35import java.util.Collections; 36import java.util.List; 37import java.util.Locale; 38 39/** 40 * Represent a logical device of type TV residing in Android system. 41 */ 42final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 43 private static final String TAG = "HdmiCecLocalDeviceTv"; 44 45 // Whether ARC is available or not. "true" means that ARC is estabilished between TV and 46 // AVR as audio receiver. 47 @ServiceThreadOnly 48 private boolean mArcEstablished = false; 49 50 // Whether ARC feature is enabled or not. 51 private boolean mArcFeatureEnabled = false; 52 53 // Whether SystemAudioMode is "On" or not. 54 @GuardedBy("mLock") 55 private boolean mSystemAudioMode; 56 57 // The previous port id (input) before switching to the new one. This is remembered in order to 58 // be able to switch to it upon receiving <Inactive Source> from currently active source. 59 // This remains valid only when the active source was switched via one touch play operation 60 // (either by TV or source device). Manual port switching invalidates this value to 61 // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything. 62 @GuardedBy("mLock") 63 private int mPrevPortId; 64 65 @GuardedBy("mLock") 66 private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME; 67 68 @GuardedBy("mLock") 69 private boolean mSystemAudioMute = false; 70 71 // Copy of mDeviceInfos to guarantee thread-safety. 72 @GuardedBy("mLock") 73 private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 74 // All external cec input(source) devices. Does not include system audio device. 75 @GuardedBy("mLock") 76 private List<HdmiCecDeviceInfo> mSafeExternalInputs = Collections.emptyList(); 77 78 // Map-like container of all cec devices including local ones. 79 // A logical address of device is used as key of container. 80 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 81 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 82 83 // If true, TV going to standby mode puts other devices also to standby. 84 private boolean mAutoDeviceOff; 85 86 HdmiCecLocalDeviceTv(HdmiControlService service) { 87 super(service, HdmiCecDeviceInfo.DEVICE_TV); 88 mPrevPortId = Constants.INVALID_PORT_ID; 89 // TODO: load system audio mode and set it to mSystemAudioMode. 90 } 91 92 @Override 93 @ServiceThreadOnly 94 protected void onAddressAllocated(int logicalAddress) { 95 assertRunOnServiceThread(); 96 // TODO: vendor-specific initialization here. 97 98 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 99 mAddress, mService.getPhysicalAddress(), mDeviceType)); 100 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 101 mAddress, mService.getVendorId())); 102 launchRoutingControl(true); 103 launchDeviceDiscovery(); 104 } 105 106 /** 107 * Performs the action 'device select', or 'one touch play' initiated by TV. 108 * 109 * @param targetAddress logical address of the device to select 110 * @param callback callback object to report the result with 111 */ 112 @ServiceThreadOnly 113 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 114 assertRunOnServiceThread(); 115 if (targetAddress == Constants.ADDR_INTERNAL) { 116 handleSelectInternalSource(callback); 117 return; 118 } 119 HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress); 120 if (targetDevice == null) { 121 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 122 return; 123 } 124 removeAction(DeviceSelectAction.class); 125 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 126 } 127 128 @ServiceThreadOnly 129 private void handleSelectInternalSource(IHdmiControlCallback callback) { 130 assertRunOnServiceThread(); 131 // Seq #18 132 if (mService.isControlEnabled() && getActiveSource() != mAddress) { 133 updateActiveSource(mAddress, mService.getPhysicalAddress()); 134 // TODO: Check if this comes from <Text/Image View On> - if true, do nothing. 135 HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource( 136 mAddress, mService.getPhysicalAddress()); 137 mService.sendCecCommand(activeSource); 138 } 139 } 140 141 @ServiceThreadOnly 142 void updateActiveSource(int activeSource, int activePath) { 143 assertRunOnServiceThread(); 144 // Seq #14 145 if (activeSource == getActiveSource() && activePath == getActivePath()) { 146 return; 147 } 148 setActiveSource(activeSource); 149 setActivePath(activePath); 150 if (getDeviceInfo(activeSource) != null && activeSource != mAddress) { 151 if (mService.pathToPortId(activePath) == getActivePortId()) { 152 setPrevPortId(getActivePortId()); 153 } 154 // TODO: Show the OSD banner related to the new active source device. 155 } else { 156 // TODO: If displayed, remove the OSD banner related to the previous 157 // active source device. 158 } 159 } 160 161 /** 162 * Returns the previous port id kept to handle input switching on <Inactive Source>. 163 */ 164 int getPrevPortId() { 165 synchronized (mLock) { 166 return mPrevPortId; 167 } 168 } 169 170 /** 171 * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be 172 * taken for <Inactive Source>. 173 */ 174 void setPrevPortId(int portId) { 175 synchronized (mLock) { 176 mPrevPortId = portId; 177 } 178 } 179 180 @ServiceThreadOnly 181 void updateActivePortId(int portId) { 182 assertRunOnServiceThread(); 183 // Seq #15 184 if (portId == getActivePortId()) { 185 return; 186 } 187 setPrevPortId(portId); 188 // TODO: Actually switch the physical port here. Handle PAP/PIP as well. 189 // Show OSD port change banner 190 mService.invokeInputChangeListener(getActiveSource()); 191 } 192 193 @ServiceThreadOnly 194 void doManualPortSwitching(int portId, IHdmiControlCallback callback) { 195 assertRunOnServiceThread(); 196 // Seq #20 197 if (!mService.isControlEnabled() || portId == getActivePortId()) { 198 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 199 return; 200 } 201 // TODO: Make sure this call does not stem from <Active Source> message reception. 202 203 setActivePortId(portId); 204 // TODO: Return immediately if the operation is triggered by <Text/Image View On> 205 // and this is the first notification about the active input after power-on. 206 // TODO: Handle invalid port id / active input which should be treated as an 207 // internal tuner. 208 209 removeAction(RoutingControlAction.class); 210 211 int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId())); 212 int newPath = mService.portIdToPath(portId); 213 HdmiCecMessage routingChange = 214 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 215 mService.sendCecCommand(routingChange); 216 addAndStartAction(new RoutingControlAction(this, newPath, false, callback)); 217 } 218 219 int getPowerStatus() { 220 return mService.getPowerStatus(); 221 } 222 223 /** 224 * Sends key to a target CEC device. 225 * 226 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 227 * @param isPressed true if this is keypress event 228 */ 229 @ServiceThreadOnly 230 void sendKeyEvent(int keyCode, boolean isPressed) { 231 assertRunOnServiceThread(); 232 List<SendKeyAction> action = getActions(SendKeyAction.class); 233 if (!action.isEmpty()) { 234 action.get(0).processKeyEvent(keyCode, isPressed); 235 } else { 236 if (isPressed) { 237 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 238 } else { 239 Slog.w(TAG, "Discard key release event"); 240 } 241 } 242 } 243 244 private static void invokeCallback(IHdmiControlCallback callback, int result) { 245 if (callback == null) { 246 return; 247 } 248 try { 249 callback.onComplete(result); 250 } catch (RemoteException e) { 251 Slog.e(TAG, "Invoking callback failed:" + e); 252 } 253 } 254 255 @Override 256 @ServiceThreadOnly 257 protected boolean handleActiveSource(HdmiCecMessage message) { 258 assertRunOnServiceThread(); 259 int address = message.getSource(); 260 int path = HdmiUtils.twoBytesToInt(message.getParams()); 261 if (getDeviceInfo(address) == null) { 262 handleNewDeviceAtTheTailOfActivePath(path); 263 } else { 264 ActiveSourceHandler.create(this, null).process(address, path); 265 } 266 return true; 267 } 268 269 @Override 270 @ServiceThreadOnly 271 protected boolean handleInactiveSource(HdmiCecMessage message) { 272 assertRunOnServiceThread(); 273 // Seq #10 274 275 // Ignore <Inactive Source> from non-active source device. 276 if (getActiveSource() != message.getSource()) { 277 return true; 278 } 279 if (isProhibitMode()) { 280 return true; 281 } 282 int portId = getPrevPortId(); 283 if (portId != Constants.INVALID_PORT_ID) { 284 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 285 286 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 287 if (inactiveSource == null) { 288 return true; 289 } 290 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 291 return true; 292 } 293 // TODO: Switch the TV freeze mode off 294 295 setActivePortId(portId); 296 doManualPortSwitching(portId, null); 297 setPrevPortId(Constants.INVALID_PORT_ID); 298 } 299 return true; 300 } 301 302 @Override 303 @ServiceThreadOnly 304 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 305 assertRunOnServiceThread(); 306 // Seq #19 307 if (mAddress == getActiveSource()) { 308 mService.sendCecCommand( 309 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 310 } 311 return true; 312 } 313 314 @Override 315 @ServiceThreadOnly 316 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 317 assertRunOnServiceThread(); 318 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 319 mAddress, Locale.getDefault().getISO3Language()); 320 // TODO: figure out how to handle failed to get language code. 321 if (command != null) { 322 mService.sendCecCommand(command); 323 } else { 324 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 325 } 326 return true; 327 } 328 329 @Override 330 @ServiceThreadOnly 331 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 332 assertRunOnServiceThread(); 333 // Ignore if [Device Discovery Action] is going on. 334 if (hasAction(DeviceDiscoveryAction.class)) { 335 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 336 + "because Device Discovery Action is on-going:" + message); 337 return true; 338 } 339 340 int path = HdmiUtils.twoBytesToInt(message.getParams()); 341 int address = message.getSource(); 342 if (!isInDeviceList(path, address)) { 343 handleNewDeviceAtTheTailOfActivePath(path); 344 } 345 addAndStartAction(new NewDeviceAction(this, address, path)); 346 return true; 347 } 348 349 private void handleNewDeviceAtTheTailOfActivePath(int path) { 350 // Seq #22 351 if (isTailOfActivePath(path, getActivePath())) { 352 removeAction(RoutingControlAction.class); 353 int newPath = mService.portIdToPath(getActivePortId()); 354 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 355 mAddress, getActivePath(), newPath)); 356 addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null)); 357 } 358 } 359 360 /** 361 * Whether the given path is located in the tail of current active path. 362 * 363 * @param path to be tested 364 * @param activePath current active path 365 * @return true if the given path is located in the tail of current active path; otherwise, 366 * false 367 */ 368 static boolean isTailOfActivePath(int path, int activePath) { 369 // If active routing path is internal source, return false. 370 if (activePath == 0) { 371 return false; 372 } 373 for (int i = 12; i >= 0; i -= 4) { 374 int curActivePath = (activePath >> i) & 0xF; 375 if (curActivePath == 0) { 376 return true; 377 } else { 378 int curPath = (path >> i) & 0xF; 379 if (curPath != curActivePath) { 380 return false; 381 } 382 } 383 } 384 return false; 385 } 386 387 @Override 388 @ServiceThreadOnly 389 protected boolean handleRoutingChange(HdmiCecMessage message) { 390 assertRunOnServiceThread(); 391 // Seq #21 392 byte[] params = message.getParams(); 393 int currentPath = HdmiUtils.twoBytesToInt(params); 394 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 395 int newPath = HdmiUtils.twoBytesToInt(params, 2); 396 setActivePath(newPath); 397 removeAction(RoutingControlAction.class); 398 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 399 } 400 return true; 401 } 402 403 @Override 404 @ServiceThreadOnly 405 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 406 assertRunOnServiceThread(); 407 408 byte params[] = message.getParams(); 409 int mute = params[0] & 0x80; 410 int volume = params[0] & 0x7F; 411 setAudioStatus(mute == 0x80, volume); 412 return true; 413 } 414 415 @Override 416 @ServiceThreadOnly 417 protected boolean handleTextViewOn(HdmiCecMessage message) { 418 assertRunOnServiceThread(); 419 if (mService.isPowerStandbyOrTransient()) { 420 mService.wakeUp(); 421 } 422 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 423 // that represents the source device. 424 return true; 425 } 426 427 @Override 428 @ServiceThreadOnly 429 protected boolean handleImageViewOn(HdmiCecMessage message) { 430 assertRunOnServiceThread(); 431 // Currently, it's the same as <Text View On>. 432 return handleTextViewOn(message); 433 } 434 435 @Override 436 @ServiceThreadOnly 437 protected boolean handleSetOsdName(HdmiCecMessage message) { 438 int source = message.getSource(); 439 HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source); 440 // If the device is not in device list, ignore it. 441 if (deviceInfo == null) { 442 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 443 return true; 444 } 445 String osdName = null; 446 try { 447 osdName = new String(message.getParams(), "US-ASCII"); 448 } catch (UnsupportedEncodingException e) { 449 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 450 return true; 451 } 452 453 if (deviceInfo.getDisplayName().equals(osdName)) { 454 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 455 return true; 456 } 457 458 addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(), 459 deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(), 460 deviceInfo.getVendorId(), osdName)); 461 return true; 462 } 463 464 @ServiceThreadOnly 465 private void launchDeviceDiscovery() { 466 assertRunOnServiceThread(); 467 clearDeviceInfoList(); 468 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 469 new DeviceDiscoveryCallback() { 470 @Override 471 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 472 for (HdmiCecDeviceInfo info : deviceInfos) { 473 addCecDevice(info); 474 } 475 476 // Since we removed all devices when it's start and 477 // device discovery action does not poll local devices, 478 // we should put device info of local device manually here 479 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 480 addCecDevice(device.getDeviceInfo()); 481 } 482 483 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 484 485 // If there is AVR, initiate System Audio Auto initiation action, 486 // which turns on and off system audio according to last system 487 // audio setting. 488 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 489 if (avrInfo != null) { 490 addAndStartAction(new SystemAudioAutoInitiationAction( 491 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 492 if (mArcEstablished) { 493 startArcAction(true); 494 } 495 } 496 } 497 }); 498 addAndStartAction(action); 499 } 500 501 // Clear all device info. 502 @ServiceThreadOnly 503 private void clearDeviceInfoList() { 504 assertRunOnServiceThread(); 505 mDeviceInfos.clear(); 506 updateSafeDeviceInfoList(); 507 } 508 509 @ServiceThreadOnly 510 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 511 assertRunOnServiceThread(); 512 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 513 if (avr == null) { 514 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 515 return; 516 } 517 518 addAndStartAction( 519 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 520 } 521 522 // # Seq 25 523 void setSystemAudioMode(boolean on) { 524 synchronized (mLock) { 525 if (on != mSystemAudioMode) { 526 mSystemAudioMode = on; 527 // TODO: Need to set the preference for SystemAudioMode. 528 mService.announceSystemAudioModeChange(on); 529 } 530 } 531 } 532 533 boolean getSystemAudioMode() { 534 synchronized (mLock) { 535 return mSystemAudioMode; 536 } 537 } 538 539 /** 540 * Change ARC status into the given {@code enabled} status. 541 * 542 * @return {@code true} if ARC was in "Enabled" status 543 */ 544 @ServiceThreadOnly 545 boolean setArcStatus(boolean enabled) { 546 assertRunOnServiceThread(); 547 boolean oldStatus = mArcEstablished; 548 // 1. Enable/disable ARC circuit. 549 mService.setAudioReturnChannel(enabled); 550 // 2. Notify arc status to audio service. 551 notifyArcStatusToAudioService(enabled); 552 // 3. Update arc status; 553 mArcEstablished = enabled; 554 return oldStatus; 555 } 556 557 private void notifyArcStatusToAudioService(boolean enabled) { 558 // Note that we don't set any name to ARC. 559 mService.getAudioManager().setWiredDeviceConnectionState( 560 AudioSystem.DEVICE_OUT_HDMI_ARC, 561 enabled ? 1 : 0, ""); 562 } 563 564 /** 565 * Returns whether ARC is enabled or not. 566 */ 567 @ServiceThreadOnly 568 boolean isArcEstabilished() { 569 assertRunOnServiceThread(); 570 return mArcFeatureEnabled && mArcEstablished; 571 } 572 573 @ServiceThreadOnly 574 void changeArcFeatureEnabled(boolean enabled) { 575 assertRunOnServiceThread(); 576 577 if (mArcFeatureEnabled != enabled) { 578 if (enabled) { 579 if (!mArcEstablished) { 580 startArcAction(true); 581 } 582 } else { 583 if (mArcEstablished) { 584 startArcAction(false); 585 } 586 } 587 mArcFeatureEnabled = enabled; 588 } 589 } 590 591 @ServiceThreadOnly 592 boolean isArcFeatureEnabled() { 593 assertRunOnServiceThread(); 594 return mArcFeatureEnabled; 595 } 596 597 @ServiceThreadOnly 598 private void startArcAction(boolean enabled) { 599 assertRunOnServiceThread(); 600 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 601 if (info == null) { 602 return; 603 } 604 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 605 return; 606 } 607 608 // Terminate opposite action and start action if not exist. 609 if (enabled) { 610 removeAction(RequestArcTerminationAction.class); 611 if (!hasAction(RequestArcInitiationAction.class)) { 612 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 613 } 614 } else { 615 removeAction(RequestArcInitiationAction.class); 616 if (!hasAction(RequestArcTerminationAction.class)) { 617 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 618 } 619 } 620 } 621 622 void setAudioStatus(boolean mute, int volume) { 623 synchronized (mLock) { 624 mSystemAudioMute = mute; 625 mSystemAudioVolume = volume; 626 // TODO: pass volume to service (audio service) after scale it to local volume level. 627 mService.setAudioStatus(mute, volume); 628 } 629 } 630 631 @ServiceThreadOnly 632 void changeVolume(int curVolume, int delta, int maxVolume) { 633 assertRunOnServiceThread(); 634 if (delta == 0 || !isSystemAudioOn()) { 635 return; 636 } 637 638 int targetVolume = curVolume + delta; 639 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 640 synchronized (mLock) { 641 // If new volume is the same as current system audio volume, just ignore it. 642 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 643 if (cecVolume == mSystemAudioVolume) { 644 // Update tv volume with system volume value. 645 mService.setAudioStatus(false, 646 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 647 return; 648 } 649 } 650 651 // Remove existing volume action. 652 removeAction(VolumeControlAction.class); 653 654 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 655 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 656 cecVolume, delta > 0)); 657 } 658 659 @ServiceThreadOnly 660 void changeMute(boolean mute) { 661 assertRunOnServiceThread(); 662 if (!isSystemAudioOn()) { 663 return; 664 } 665 666 // Remove existing volume action. 667 removeAction(VolumeControlAction.class); 668 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 669 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 670 } 671 672 private boolean isSystemAudioOn() { 673 if (getAvrDeviceInfo() == null) { 674 return false; 675 } 676 677 synchronized (mLock) { 678 return mSystemAudioMode; 679 } 680 } 681 682 @Override 683 @ServiceThreadOnly 684 protected boolean handleInitiateArc(HdmiCecMessage message) { 685 assertRunOnServiceThread(); 686 // In case where <Initiate Arc> is started by <Request ARC Initiation> 687 // need to clean up RequestArcInitiationAction. 688 removeAction(RequestArcInitiationAction.class); 689 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 690 message.getSource(), true); 691 addAndStartAction(action); 692 return true; 693 } 694 695 @Override 696 @ServiceThreadOnly 697 protected boolean handleTerminateArc(HdmiCecMessage message) { 698 assertRunOnServiceThread(); 699 // In case where <Terminate Arc> is started by <Request ARC Termination> 700 // need to clean up RequestArcInitiationAction. 701 removeAction(RequestArcTerminationAction.class); 702 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 703 message.getSource(), false); 704 addAndStartAction(action); 705 return true; 706 } 707 708 @Override 709 @ServiceThreadOnly 710 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 711 assertRunOnServiceThread(); 712 if (!isMessageForSystemAudio(message)) { 713 return false; 714 } 715 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 716 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 717 addAndStartAction(action); 718 return true; 719 } 720 721 @Override 722 @ServiceThreadOnly 723 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 724 assertRunOnServiceThread(); 725 if (!isMessageForSystemAudio(message)) { 726 return false; 727 } 728 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 729 return true; 730 } 731 732 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 733 if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM 734 || message.getDestination() != Constants.ADDR_TV 735 || getAvrDeviceInfo() == null) { 736 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 737 return false; 738 } 739 return true; 740 } 741 742 /** 743 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 744 * logical address as new device info's. 745 * 746 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 747 * 748 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 749 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 750 * that has the same logical address as new one has. 751 */ 752 @ServiceThreadOnly 753 private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 754 assertRunOnServiceThread(); 755 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 756 if (oldDeviceInfo != null) { 757 removeDeviceInfo(deviceInfo.getLogicalAddress()); 758 } 759 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 760 updateSafeDeviceInfoList(); 761 return oldDeviceInfo; 762 } 763 764 /** 765 * Remove a device info corresponding to the given {@code logicalAddress}. 766 * It returns removed {@link HdmiCecDeviceInfo} if exists. 767 * 768 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 769 * 770 * @param logicalAddress logical address of device to be removed 771 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 772 */ 773 @ServiceThreadOnly 774 private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 775 assertRunOnServiceThread(); 776 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 777 if (deviceInfo != null) { 778 mDeviceInfos.remove(logicalAddress); 779 } 780 updateSafeDeviceInfoList(); 781 return deviceInfo; 782 } 783 784 /** 785 * Return a list of all {@link HdmiCecDeviceInfo}. 786 * 787 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 788 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 789 */ 790 @ServiceThreadOnly 791 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 792 assertRunOnServiceThread(); 793 if (includelLocalDevice) { 794 return HdmiUtils.sparseArrayToList(mDeviceInfos); 795 } else { 796 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 797 for (int i = 0; i < mDeviceInfos.size(); ++i) { 798 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 799 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 800 infoList.add(info); 801 } 802 } 803 return infoList; 804 } 805 } 806 807 /** 808 * Return external input devices. 809 */ 810 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 811 synchronized (mLock) { 812 return mSafeExternalInputs; 813 } 814 } 815 816 @ServiceThreadOnly 817 private void updateSafeDeviceInfoList() { 818 assertRunOnServiceThread(); 819 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 820 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 821 synchronized (mLock) { 822 mSafeAllDeviceInfos = copiedDevices; 823 mSafeExternalInputs = externalInputs; 824 } 825 } 826 827 /** 828 * Return a list of external cec input (source) devices. 829 * 830 * <p>Note that this effectively excludes non-source devices like system audio, 831 * secondary TV. 832 */ 833 private List<HdmiCecDeviceInfo> getInputDevices() { 834 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 835 for (int i = 0; i < mDeviceInfos.size(); ++i) { 836 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 837 if (isLocalDeviceAddress(i)) { 838 continue; 839 } 840 if (info.isSourceType()) { 841 infoList.add(info); 842 } 843 } 844 return infoList; 845 } 846 847 @ServiceThreadOnly 848 private boolean isLocalDeviceAddress(int address) { 849 assertRunOnServiceThread(); 850 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 851 if (device.isAddressOf(address)) { 852 return true; 853 } 854 } 855 return false; 856 } 857 858 @ServiceThreadOnly 859 HdmiCecDeviceInfo getAvrDeviceInfo() { 860 assertRunOnServiceThread(); 861 return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 862 } 863 864 /** 865 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 866 * 867 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 868 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 869 * 870 * @param logicalAddress logical address to be retrieved 871 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 872 * Returns null if no logical address matched 873 */ 874 @ServiceThreadOnly 875 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 876 assertRunOnServiceThread(); 877 return mDeviceInfos.get(logicalAddress); 878 } 879 880 boolean hasSystemAudioDevice() { 881 return getSafeAvrDeviceInfo() != null; 882 } 883 884 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 885 return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 886 } 887 888 /** 889 * Thread safe version of {@link #getDeviceInfo(int)}. 890 * 891 * @param logicalAddress logical address to be retrieved 892 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 893 * Returns null if no logical address matched 894 */ 895 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 896 synchronized (mLock) { 897 return mSafeAllDeviceInfos.get(logicalAddress); 898 } 899 } 900 901 /** 902 * Called when a device is newly added or a new device is detected or 903 * existing device is updated. 904 * 905 * @param info device info of a new device. 906 */ 907 @ServiceThreadOnly 908 final void addCecDevice(HdmiCecDeviceInfo info) { 909 assertRunOnServiceThread(); 910 addDeviceInfo(info); 911 if (info.getLogicalAddress() == mAddress) { 912 // The addition of TV device itself should not be notified. 913 return; 914 } 915 mService.invokeDeviceEventListeners(info, true); 916 } 917 918 /** 919 * Called when a device is removed or removal of device is detected. 920 * 921 * @param address a logical address of a device to be removed 922 */ 923 @ServiceThreadOnly 924 final void removeCecDevice(int address) { 925 assertRunOnServiceThread(); 926 HdmiCecDeviceInfo info = removeDeviceInfo(address); 927 928 mCecMessageCache.flushMessagesFrom(address); 929 mService.invokeDeviceEventListeners(info, false); 930 } 931 932 @ServiceThreadOnly 933 void handleRemoveActiveRoutingPath(int path) { 934 assertRunOnServiceThread(); 935 // Seq #23 936 if (isTailOfActivePath(path, getActivePath())) { 937 removeAction(RoutingControlAction.class); 938 int newPath = mService.portIdToPath(getActivePortId()); 939 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 940 mAddress, getActivePath(), newPath)); 941 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 942 } 943 } 944 945 /** 946 * Launch routing control process. 947 * 948 * @param routingForBootup true if routing control is initiated due to One Touch Play 949 * or TV power on 950 */ 951 @ServiceThreadOnly 952 void launchRoutingControl(boolean routingForBootup) { 953 assertRunOnServiceThread(); 954 // Seq #24 955 if (getActivePortId() != Constants.INVALID_PORT_ID) { 956 if (!routingForBootup && !isProhibitMode()) { 957 removeAction(RoutingControlAction.class); 958 int newPath = mService.portIdToPath(getActivePortId()); 959 setActivePath(newPath); 960 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 961 getActivePath(), newPath)); 962 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 963 routingForBootup, null)); 964 } 965 } else { 966 int activePath = mService.getPhysicalAddress(); 967 setActivePath(activePath); 968 if (!routingForBootup) { 969 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 970 activePath)); 971 } 972 } 973 } 974 975 /** 976 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 977 * the given routing path. CEC devices use routing path for its physical address to 978 * describe the hierarchy of the devices in the network. 979 * 980 * @param path routing path or physical address 981 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 982 */ 983 @ServiceThreadOnly 984 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 985 assertRunOnServiceThread(); 986 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 987 if (info.getPhysicalAddress() == path) { 988 return info; 989 } 990 } 991 return null; 992 } 993 994 /** 995 * Whether a device of the specified physical address and logical address exists 996 * in a device info list. However, both are minimal condition and it could 997 * be different device from the original one. 998 * 999 * @param logicalAddress logical address of a device to be searched 1000 * @param physicalAddress physical address of a device to be searched 1001 * @return true if exist; otherwise false 1002 */ 1003 @ServiceThreadOnly 1004 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1005 assertRunOnServiceThread(); 1006 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1007 if (device == null) { 1008 return false; 1009 } 1010 return device.getPhysicalAddress() == physicalAddress; 1011 } 1012 1013 @Override 1014 @ServiceThreadOnly 1015 void onHotplug(int portId, boolean connected) { 1016 assertRunOnServiceThread(); 1017 1018 // Tv device will have permanent HotplugDetectionAction. 1019 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1020 if (!hotplugActions.isEmpty()) { 1021 // Note that hotplug action is single action running on a machine. 1022 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1023 // It covers seq #40, #43. 1024 hotplugActions.get(0).pollAllDevicesNow(); 1025 } 1026 } 1027 1028 @ServiceThreadOnly 1029 void setAutoDeviceOff(boolean enabled) { 1030 assertRunOnServiceThread(); 1031 mAutoDeviceOff = enabled; 1032 } 1033 1034 @Override 1035 @ServiceThreadOnly 1036 protected void onTransitionToStandby(boolean initiatedByCec) { 1037 assertRunOnServiceThread(); 1038 // Remove any repeated working actions. 1039 // HotplugDetectionAction will be reinstated during the wake up process. 1040 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1041 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1042 removeAction(HotplugDetectionAction.class); 1043 checkIfPendingActionsCleared(); 1044 } 1045 1046 @Override 1047 @ServiceThreadOnly 1048 protected void onStandBy(boolean initiatedByCec) { 1049 assertRunOnServiceThread(); 1050 // Seq #11 1051 if (!mService.isControlEnabled()) { 1052 return; 1053 } 1054 if (!initiatedByCec) { 1055 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1056 mAddress, Constants.ADDR_BROADCAST)); 1057 } 1058 } 1059 1060 @Override 1061 @ServiceThreadOnly 1062 protected boolean handleStandby(HdmiCecMessage message) { 1063 assertRunOnServiceThread(); 1064 // Seq #12 1065 // Tv accepts directly addressed <Standby> only. 1066 if (message.getDestination() == mAddress) { 1067 super.handleStandby(message); 1068 } 1069 return false; 1070 } 1071 1072 boolean isProhibitMode() { 1073 return mService.isProhibitMode(); 1074 } 1075 1076 boolean isPowerStandbyOrTransient() { 1077 return mService.isPowerStandbyOrTransient(); 1078 } 1079 1080 void displayOsd(int messageId) { 1081 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1082 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1083 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1084 HdmiControlService.PERMISSION); 1085 } 1086} 1087