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