HdmiCecLocalDeviceTv.java revision 4fc1d105fc279bf7df6c876e160672866bdad8e7
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 key press event 228 */ 229 @Override 230 @ServiceThreadOnly 231 protected void sendKeyEvent(int keyCode, boolean isPressed) { 232 assertRunOnServiceThread(); 233 List<SendKeyAction> action = getActions(SendKeyAction.class); 234 if (!action.isEmpty()) { 235 action.get(0).processKeyEvent(keyCode, isPressed); 236 } else { 237 if (isPressed) { 238 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 239 } else { 240 Slog.w(TAG, "Discard key release event"); 241 } 242 } 243 } 244 245 private static void invokeCallback(IHdmiControlCallback callback, int result) { 246 if (callback == null) { 247 return; 248 } 249 try { 250 callback.onComplete(result); 251 } catch (RemoteException e) { 252 Slog.e(TAG, "Invoking callback failed:" + e); 253 } 254 } 255 256 @Override 257 @ServiceThreadOnly 258 protected boolean handleActiveSource(HdmiCecMessage message) { 259 assertRunOnServiceThread(); 260 int address = message.getSource(); 261 int path = HdmiUtils.twoBytesToInt(message.getParams()); 262 if (getDeviceInfo(address) == null) { 263 handleNewDeviceAtTheTailOfActivePath(path); 264 } else { 265 ActiveSourceHandler.create(this, null).process(address, path); 266 } 267 return true; 268 } 269 270 @Override 271 @ServiceThreadOnly 272 protected boolean handleInactiveSource(HdmiCecMessage message) { 273 assertRunOnServiceThread(); 274 // Seq #10 275 276 // Ignore <Inactive Source> from non-active source device. 277 if (getActiveSource() != message.getSource()) { 278 return true; 279 } 280 if (isProhibitMode()) { 281 return true; 282 } 283 int portId = getPrevPortId(); 284 if (portId != Constants.INVALID_PORT_ID) { 285 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 286 287 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 288 if (inactiveSource == null) { 289 return true; 290 } 291 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 292 return true; 293 } 294 // TODO: Switch the TV freeze mode off 295 296 setActivePortId(portId); 297 doManualPortSwitching(portId, null); 298 setPrevPortId(Constants.INVALID_PORT_ID); 299 } 300 return true; 301 } 302 303 @Override 304 @ServiceThreadOnly 305 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 306 assertRunOnServiceThread(); 307 // Seq #19 308 if (mAddress == getActiveSource()) { 309 mService.sendCecCommand( 310 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 311 } 312 return true; 313 } 314 315 @Override 316 @ServiceThreadOnly 317 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 318 assertRunOnServiceThread(); 319 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 320 mAddress, Locale.getDefault().getISO3Language()); 321 // TODO: figure out how to handle failed to get language code. 322 if (command != null) { 323 mService.sendCecCommand(command); 324 } else { 325 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 326 } 327 return true; 328 } 329 330 @Override 331 @ServiceThreadOnly 332 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 333 assertRunOnServiceThread(); 334 // Ignore if [Device Discovery Action] is going on. 335 if (hasAction(DeviceDiscoveryAction.class)) { 336 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 337 + "because Device Discovery Action is on-going:" + message); 338 return true; 339 } 340 341 int path = HdmiUtils.twoBytesToInt(message.getParams()); 342 int address = message.getSource(); 343 if (!isInDeviceList(path, address)) { 344 handleNewDeviceAtTheTailOfActivePath(path); 345 } 346 addAndStartAction(new NewDeviceAction(this, address, path)); 347 return true; 348 } 349 350 private void handleNewDeviceAtTheTailOfActivePath(int path) { 351 // Seq #22 352 if (isTailOfActivePath(path, getActivePath())) { 353 removeAction(RoutingControlAction.class); 354 int newPath = mService.portIdToPath(getActivePortId()); 355 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 356 mAddress, getActivePath(), newPath)); 357 addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null)); 358 } 359 } 360 361 /** 362 * Whether the given path is located in the tail of current active path. 363 * 364 * @param path to be tested 365 * @param activePath current active path 366 * @return true if the given path is located in the tail of current active path; otherwise, 367 * false 368 */ 369 static boolean isTailOfActivePath(int path, int activePath) { 370 // If active routing path is internal source, return false. 371 if (activePath == 0) { 372 return false; 373 } 374 for (int i = 12; i >= 0; i -= 4) { 375 int curActivePath = (activePath >> i) & 0xF; 376 if (curActivePath == 0) { 377 return true; 378 } else { 379 int curPath = (path >> i) & 0xF; 380 if (curPath != curActivePath) { 381 return false; 382 } 383 } 384 } 385 return false; 386 } 387 388 @Override 389 @ServiceThreadOnly 390 protected boolean handleRoutingChange(HdmiCecMessage message) { 391 assertRunOnServiceThread(); 392 // Seq #21 393 byte[] params = message.getParams(); 394 int currentPath = HdmiUtils.twoBytesToInt(params); 395 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 396 int newPath = HdmiUtils.twoBytesToInt(params, 2); 397 setActivePath(newPath); 398 removeAction(RoutingControlAction.class); 399 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 400 } 401 return true; 402 } 403 404 @Override 405 @ServiceThreadOnly 406 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 407 assertRunOnServiceThread(); 408 409 byte params[] = message.getParams(); 410 int mute = params[0] & 0x80; 411 int volume = params[0] & 0x7F; 412 setAudioStatus(mute == 0x80, volume); 413 return true; 414 } 415 416 @Override 417 @ServiceThreadOnly 418 protected boolean handleTextViewOn(HdmiCecMessage message) { 419 assertRunOnServiceThread(); 420 if (mService.isPowerStandbyOrTransient()) { 421 mService.wakeUp(); 422 } 423 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 424 // that represents the source device. 425 return true; 426 } 427 428 @Override 429 @ServiceThreadOnly 430 protected boolean handleImageViewOn(HdmiCecMessage message) { 431 assertRunOnServiceThread(); 432 // Currently, it's the same as <Text View On>. 433 return handleTextViewOn(message); 434 } 435 436 @Override 437 @ServiceThreadOnly 438 protected boolean handleSetOsdName(HdmiCecMessage message) { 439 int source = message.getSource(); 440 HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source); 441 // If the device is not in device list, ignore it. 442 if (deviceInfo == null) { 443 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 444 return true; 445 } 446 String osdName = null; 447 try { 448 osdName = new String(message.getParams(), "US-ASCII"); 449 } catch (UnsupportedEncodingException e) { 450 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 451 return true; 452 } 453 454 if (deviceInfo.getDisplayName().equals(osdName)) { 455 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 456 return true; 457 } 458 459 addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(), 460 deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(), 461 deviceInfo.getVendorId(), osdName)); 462 return true; 463 } 464 465 @ServiceThreadOnly 466 private void launchDeviceDiscovery() { 467 assertRunOnServiceThread(); 468 clearDeviceInfoList(); 469 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 470 new DeviceDiscoveryCallback() { 471 @Override 472 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 473 for (HdmiCecDeviceInfo info : deviceInfos) { 474 addCecDevice(info); 475 } 476 477 // Since we removed all devices when it's start and 478 // device discovery action does not poll local devices, 479 // we should put device info of local device manually here 480 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 481 addCecDevice(device.getDeviceInfo()); 482 } 483 484 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 485 486 // If there is AVR, initiate System Audio Auto initiation action, 487 // which turns on and off system audio according to last system 488 // audio setting. 489 HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo(); 490 if (avrInfo != null) { 491 addAndStartAction(new SystemAudioAutoInitiationAction( 492 HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress())); 493 if (mArcEstablished) { 494 startArcAction(true); 495 } 496 } 497 } 498 }); 499 addAndStartAction(action); 500 } 501 502 // Clear all device info. 503 @ServiceThreadOnly 504 private void clearDeviceInfoList() { 505 assertRunOnServiceThread(); 506 mDeviceInfos.clear(); 507 updateSafeDeviceInfoList(); 508 } 509 510 @ServiceThreadOnly 511 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 512 assertRunOnServiceThread(); 513 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 514 if (avr == null) { 515 invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE); 516 return; 517 } 518 519 addAndStartAction( 520 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 521 } 522 523 // # Seq 25 524 void setSystemAudioMode(boolean on) { 525 synchronized (mLock) { 526 if (on != mSystemAudioMode) { 527 mSystemAudioMode = on; 528 // TODO: Need to set the preference for SystemAudioMode. 529 mService.announceSystemAudioModeChange(on); 530 } 531 } 532 } 533 534 boolean getSystemAudioMode() { 535 synchronized (mLock) { 536 return mSystemAudioMode; 537 } 538 } 539 540 /** 541 * Change ARC status into the given {@code enabled} status. 542 * 543 * @return {@code true} if ARC was in "Enabled" status 544 */ 545 @ServiceThreadOnly 546 boolean setArcStatus(boolean enabled) { 547 assertRunOnServiceThread(); 548 boolean oldStatus = mArcEstablished; 549 // 1. Enable/disable ARC circuit. 550 mService.setAudioReturnChannel(enabled); 551 // 2. Notify arc status to audio service. 552 notifyArcStatusToAudioService(enabled); 553 // 3. Update arc status; 554 mArcEstablished = enabled; 555 return oldStatus; 556 } 557 558 private void notifyArcStatusToAudioService(boolean enabled) { 559 // Note that we don't set any name to ARC. 560 mService.getAudioManager().setWiredDeviceConnectionState( 561 AudioSystem.DEVICE_OUT_HDMI_ARC, 562 enabled ? 1 : 0, ""); 563 } 564 565 /** 566 * Returns whether ARC is enabled or not. 567 */ 568 @ServiceThreadOnly 569 boolean isArcEstabilished() { 570 assertRunOnServiceThread(); 571 return mArcFeatureEnabled && mArcEstablished; 572 } 573 574 @ServiceThreadOnly 575 void changeArcFeatureEnabled(boolean enabled) { 576 assertRunOnServiceThread(); 577 578 if (mArcFeatureEnabled != enabled) { 579 if (enabled) { 580 if (!mArcEstablished) { 581 startArcAction(true); 582 } 583 } else { 584 if (mArcEstablished) { 585 startArcAction(false); 586 } 587 } 588 mArcFeatureEnabled = enabled; 589 } 590 } 591 592 @ServiceThreadOnly 593 boolean isArcFeatureEnabled() { 594 assertRunOnServiceThread(); 595 return mArcFeatureEnabled; 596 } 597 598 @ServiceThreadOnly 599 private void startArcAction(boolean enabled) { 600 assertRunOnServiceThread(); 601 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 602 if (info == null) { 603 return; 604 } 605 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 606 return; 607 } 608 609 // Terminate opposite action and start action if not exist. 610 if (enabled) { 611 removeAction(RequestArcTerminationAction.class); 612 if (!hasAction(RequestArcInitiationAction.class)) { 613 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 614 } 615 } else { 616 removeAction(RequestArcInitiationAction.class); 617 if (!hasAction(RequestArcTerminationAction.class)) { 618 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 619 } 620 } 621 } 622 623 void setAudioStatus(boolean mute, int volume) { 624 synchronized (mLock) { 625 mSystemAudioMute = mute; 626 mSystemAudioVolume = volume; 627 // TODO: pass volume to service (audio service) after scale it to local volume level. 628 mService.setAudioStatus(mute, volume); 629 } 630 } 631 632 @ServiceThreadOnly 633 void changeVolume(int curVolume, int delta, int maxVolume) { 634 assertRunOnServiceThread(); 635 if (delta == 0 || !isSystemAudioOn()) { 636 return; 637 } 638 639 int targetVolume = curVolume + delta; 640 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 641 synchronized (mLock) { 642 // If new volume is the same as current system audio volume, just ignore it. 643 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 644 if (cecVolume == mSystemAudioVolume) { 645 // Update tv volume with system volume value. 646 mService.setAudioStatus(false, 647 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 648 return; 649 } 650 } 651 652 // Remove existing volume action. 653 removeAction(VolumeControlAction.class); 654 655 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 656 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 657 cecVolume, delta > 0)); 658 } 659 660 @ServiceThreadOnly 661 void changeMute(boolean mute) { 662 assertRunOnServiceThread(); 663 if (!isSystemAudioOn()) { 664 return; 665 } 666 667 // Remove existing volume action. 668 removeAction(VolumeControlAction.class); 669 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 670 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 671 } 672 673 private boolean isSystemAudioOn() { 674 if (getAvrDeviceInfo() == null) { 675 return false; 676 } 677 678 synchronized (mLock) { 679 return mSystemAudioMode; 680 } 681 } 682 683 @Override 684 @ServiceThreadOnly 685 protected boolean handleInitiateArc(HdmiCecMessage message) { 686 assertRunOnServiceThread(); 687 // In case where <Initiate Arc> is started by <Request ARC Initiation> 688 // need to clean up RequestArcInitiationAction. 689 removeAction(RequestArcInitiationAction.class); 690 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 691 message.getSource(), true); 692 addAndStartAction(action); 693 return true; 694 } 695 696 @Override 697 @ServiceThreadOnly 698 protected boolean handleTerminateArc(HdmiCecMessage message) { 699 assertRunOnServiceThread(); 700 // In case where <Terminate Arc> is started by <Request ARC Termination> 701 // need to clean up RequestArcInitiationAction. 702 removeAction(RequestArcTerminationAction.class); 703 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 704 message.getSource(), false); 705 addAndStartAction(action); 706 return true; 707 } 708 709 @Override 710 @ServiceThreadOnly 711 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 712 assertRunOnServiceThread(); 713 if (!isMessageForSystemAudio(message)) { 714 return false; 715 } 716 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 717 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 718 addAndStartAction(action); 719 return true; 720 } 721 722 @Override 723 @ServiceThreadOnly 724 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 725 assertRunOnServiceThread(); 726 if (!isMessageForSystemAudio(message)) { 727 return false; 728 } 729 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message)); 730 return true; 731 } 732 733 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 734 if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM 735 || message.getDestination() != Constants.ADDR_TV 736 || getAvrDeviceInfo() == null) { 737 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 738 return false; 739 } 740 return true; 741 } 742 743 /** 744 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 745 * logical address as new device info's. 746 * 747 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 748 * 749 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 750 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 751 * that has the same logical address as new one has. 752 */ 753 @ServiceThreadOnly 754 private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 755 assertRunOnServiceThread(); 756 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 757 if (oldDeviceInfo != null) { 758 removeDeviceInfo(deviceInfo.getLogicalAddress()); 759 } 760 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 761 updateSafeDeviceInfoList(); 762 return oldDeviceInfo; 763 } 764 765 /** 766 * Remove a device info corresponding to the given {@code logicalAddress}. 767 * It returns removed {@link HdmiCecDeviceInfo} if exists. 768 * 769 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 770 * 771 * @param logicalAddress logical address of device to be removed 772 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 773 */ 774 @ServiceThreadOnly 775 private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 776 assertRunOnServiceThread(); 777 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 778 if (deviceInfo != null) { 779 mDeviceInfos.remove(logicalAddress); 780 } 781 updateSafeDeviceInfoList(); 782 return deviceInfo; 783 } 784 785 /** 786 * Return a list of all {@link HdmiCecDeviceInfo}. 787 * 788 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 789 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 790 */ 791 @ServiceThreadOnly 792 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 793 assertRunOnServiceThread(); 794 if (includelLocalDevice) { 795 return HdmiUtils.sparseArrayToList(mDeviceInfos); 796 } else { 797 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 798 for (int i = 0; i < mDeviceInfos.size(); ++i) { 799 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 800 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 801 infoList.add(info); 802 } 803 } 804 return infoList; 805 } 806 } 807 808 /** 809 * Return external input devices. 810 */ 811 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 812 synchronized (mLock) { 813 return mSafeExternalInputs; 814 } 815 } 816 817 @ServiceThreadOnly 818 private void updateSafeDeviceInfoList() { 819 assertRunOnServiceThread(); 820 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 821 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 822 synchronized (mLock) { 823 mSafeAllDeviceInfos = copiedDevices; 824 mSafeExternalInputs = externalInputs; 825 } 826 } 827 828 /** 829 * Return a list of external cec input (source) devices. 830 * 831 * <p>Note that this effectively excludes non-source devices like system audio, 832 * secondary TV. 833 */ 834 private List<HdmiCecDeviceInfo> getInputDevices() { 835 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 836 for (int i = 0; i < mDeviceInfos.size(); ++i) { 837 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 838 if (isLocalDeviceAddress(i)) { 839 continue; 840 } 841 if (info.isSourceType()) { 842 infoList.add(info); 843 } 844 } 845 return infoList; 846 } 847 848 @ServiceThreadOnly 849 private boolean isLocalDeviceAddress(int address) { 850 assertRunOnServiceThread(); 851 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 852 if (device.isAddressOf(address)) { 853 return true; 854 } 855 } 856 return false; 857 } 858 859 @ServiceThreadOnly 860 HdmiCecDeviceInfo getAvrDeviceInfo() { 861 assertRunOnServiceThread(); 862 return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 863 } 864 865 /** 866 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 867 * 868 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 869 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 870 * 871 * @param logicalAddress logical address to be retrieved 872 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 873 * Returns null if no logical address matched 874 */ 875 @ServiceThreadOnly 876 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 877 assertRunOnServiceThread(); 878 return mDeviceInfos.get(logicalAddress); 879 } 880 881 boolean hasSystemAudioDevice() { 882 return getSafeAvrDeviceInfo() != null; 883 } 884 885 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 886 return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 887 } 888 889 /** 890 * Thread safe version of {@link #getDeviceInfo(int)}. 891 * 892 * @param logicalAddress logical address to be retrieved 893 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 894 * Returns null if no logical address matched 895 */ 896 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 897 synchronized (mLock) { 898 return mSafeAllDeviceInfos.get(logicalAddress); 899 } 900 } 901 902 /** 903 * Called when a device is newly added or a new device is detected or 904 * existing device is updated. 905 * 906 * @param info device info of a new device. 907 */ 908 @ServiceThreadOnly 909 final void addCecDevice(HdmiCecDeviceInfo info) { 910 assertRunOnServiceThread(); 911 addDeviceInfo(info); 912 if (info.getLogicalAddress() == mAddress) { 913 // The addition of TV device itself should not be notified. 914 return; 915 } 916 mService.invokeDeviceEventListeners(info, true); 917 } 918 919 /** 920 * Called when a device is removed or removal of device is detected. 921 * 922 * @param address a logical address of a device to be removed 923 */ 924 @ServiceThreadOnly 925 final void removeCecDevice(int address) { 926 assertRunOnServiceThread(); 927 HdmiCecDeviceInfo info = removeDeviceInfo(address); 928 929 mCecMessageCache.flushMessagesFrom(address); 930 mService.invokeDeviceEventListeners(info, false); 931 } 932 933 @ServiceThreadOnly 934 void handleRemoveActiveRoutingPath(int path) { 935 assertRunOnServiceThread(); 936 // Seq #23 937 if (isTailOfActivePath(path, getActivePath())) { 938 removeAction(RoutingControlAction.class); 939 int newPath = mService.portIdToPath(getActivePortId()); 940 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 941 mAddress, getActivePath(), newPath)); 942 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 943 } 944 } 945 946 /** 947 * Launch routing control process. 948 * 949 * @param routingForBootup true if routing control is initiated due to One Touch Play 950 * or TV power on 951 */ 952 @ServiceThreadOnly 953 void launchRoutingControl(boolean routingForBootup) { 954 assertRunOnServiceThread(); 955 // Seq #24 956 if (getActivePortId() != Constants.INVALID_PORT_ID) { 957 if (!routingForBootup && !isProhibitMode()) { 958 removeAction(RoutingControlAction.class); 959 int newPath = mService.portIdToPath(getActivePortId()); 960 setActivePath(newPath); 961 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 962 getActivePath(), newPath)); 963 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 964 routingForBootup, null)); 965 } 966 } else { 967 int activePath = mService.getPhysicalAddress(); 968 setActivePath(activePath); 969 if (!routingForBootup) { 970 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 971 activePath)); 972 } 973 } 974 } 975 976 /** 977 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 978 * the given routing path. CEC devices use routing path for its physical address to 979 * describe the hierarchy of the devices in the network. 980 * 981 * @param path routing path or physical address 982 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 983 */ 984 @ServiceThreadOnly 985 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 986 assertRunOnServiceThread(); 987 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 988 if (info.getPhysicalAddress() == path) { 989 return info; 990 } 991 } 992 return null; 993 } 994 995 /** 996 * Whether a device of the specified physical address and logical address exists 997 * in a device info list. However, both are minimal condition and it could 998 * be different device from the original one. 999 * 1000 * @param logicalAddress logical address of a device to be searched 1001 * @param physicalAddress physical address of a device to be searched 1002 * @return true if exist; otherwise false 1003 */ 1004 @ServiceThreadOnly 1005 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1006 assertRunOnServiceThread(); 1007 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1008 if (device == null) { 1009 return false; 1010 } 1011 return device.getPhysicalAddress() == physicalAddress; 1012 } 1013 1014 @Override 1015 @ServiceThreadOnly 1016 void onHotplug(int portId, boolean connected) { 1017 assertRunOnServiceThread(); 1018 1019 // Tv device will have permanent HotplugDetectionAction. 1020 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1021 if (!hotplugActions.isEmpty()) { 1022 // Note that hotplug action is single action running on a machine. 1023 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1024 // It covers seq #40, #43. 1025 hotplugActions.get(0).pollAllDevicesNow(); 1026 } 1027 } 1028 1029 @ServiceThreadOnly 1030 void setAutoDeviceOff(boolean enabled) { 1031 assertRunOnServiceThread(); 1032 mAutoDeviceOff = enabled; 1033 } 1034 1035 @Override 1036 @ServiceThreadOnly 1037 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1038 super.disableDevice(initiatedByCec, callback); 1039 assertRunOnServiceThread(); 1040 // Remove any repeated working actions. 1041 // HotplugDetectionAction will be reinstated during the wake up process. 1042 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1043 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1044 removeAction(DeviceDiscoveryAction.class); 1045 removeAction(HotplugDetectionAction.class); 1046 1047 disableSystemAudioIfExist(); 1048 disableArcIfExist(); 1049 checkIfPendingActionsCleared(); 1050 } 1051 1052 @ServiceThreadOnly 1053 private void disableSystemAudioIfExist() { 1054 assertRunOnServiceThread(); 1055 if (getAvrDeviceInfo() == null) { 1056 return; 1057 } 1058 1059 // Seq #31. 1060 removeAction(SystemAudioActionFromAvr.class); 1061 removeAction(SystemAudioActionFromTv.class); 1062 removeAction(SystemAudioAutoInitiationAction.class); 1063 removeAction(SystemAudioStatusAction.class); 1064 removeAction(VolumeControlAction.class); 1065 1066 // Once adding additional param which describes whether to record it to NVM or not to this 1067 // method, put "false" for it. 1068 setSystemAudioMode(false); 1069 } 1070 1071 @ServiceThreadOnly 1072 private void disableArcIfExist() { 1073 assertRunOnServiceThread(); 1074 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 1075 if (avr == null) { 1076 return; 1077 } 1078 1079 // Seq #44. 1080 removeAction(RequestArcInitiationAction.class); 1081 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1082 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1083 } 1084 } 1085 1086 @Override 1087 @ServiceThreadOnly 1088 protected void onStandby(boolean initiatedByCec) { 1089 assertRunOnServiceThread(); 1090 // Seq #11 1091 if (!mService.isControlEnabled()) { 1092 return; 1093 } 1094 if (!initiatedByCec) { 1095 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1096 mAddress, Constants.ADDR_BROADCAST)); 1097 } 1098 } 1099 1100 @Override 1101 @ServiceThreadOnly 1102 protected boolean handleStandby(HdmiCecMessage message) { 1103 assertRunOnServiceThread(); 1104 // Seq #12 1105 // Tv accepts directly addressed <Standby> only. 1106 if (message.getDestination() == mAddress) { 1107 super.handleStandby(message); 1108 } 1109 return false; 1110 } 1111 1112 boolean isProhibitMode() { 1113 return mService.isProhibitMode(); 1114 } 1115 1116 boolean isPowerStandbyOrTransient() { 1117 return mService.isPowerStandbyOrTransient(); 1118 } 1119 1120 void displayOsd(int messageId) { 1121 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1122 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1123 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1124 HdmiControlService.PERMISSION); 1125 } 1126} 1127