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