1 /* 2 * Copyright (C) 2020 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 17 package com.android.server.hdmi; 18 19 import static com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 20 21 import android.annotation.Nullable; 22 import android.hardware.hdmi.DeviceFeatures; 23 import android.hardware.hdmi.HdmiControlManager; 24 import android.hardware.hdmi.HdmiDeviceInfo; 25 import android.hardware.hdmi.HdmiPortInfo; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.util.ArraySet; 29 import android.util.Slog; 30 import android.util.SparseArray; 31 import android.util.SparseIntArray; 32 33 import com.android.internal.annotations.GuardedBy; 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.util.IndentingPrintWriter; 36 37 import java.io.UnsupportedEncodingException; 38 import java.text.SimpleDateFormat; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.Iterator; 44 import java.util.List; 45 import java.util.concurrent.ArrayBlockingQueue; 46 47 /** 48 * Holds information about the current state of the HDMI CEC network. It is the sole source of 49 * truth for device information in the CEC network. 50 * 51 * This information includes: 52 * - All local devices 53 * - All HDMI ports, their capabilities and status 54 * - All devices connected to the CEC bus 55 * 56 * This class receives all incoming CEC messages and passively listens to device updates to fill 57 * out the above information. 58 * This class should not take any active action in sending CEC messages. 59 * 60 * Note that the information cached in this class is not guaranteed to be up-to-date, especially OSD 61 * names, power states can be outdated. For local devices, more up-to-date information can be 62 * accessed through {@link HdmiCecLocalDevice#getDeviceInfo()}. 63 */ 64 @VisibleForTesting 65 public class HdmiCecNetwork { 66 private static final String TAG = "HdmiCecNetwork"; 67 68 protected final Object mLock; 69 private final HdmiControlService mHdmiControlService; 70 private final HdmiCecController mHdmiCecController; 71 private final HdmiMhlControllerStub mHdmiMhlController; 72 private final Handler mHandler; 73 // Stores the local CEC devices in the system. Device type is used for key. 74 private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>(); 75 76 // Map-like container of all cec devices including local ones. 77 // device id is used as key of container. 78 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 79 private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>(); 80 // Set of physical addresses of CEC switches on the CEC bus. Managed independently from 81 // other CEC devices since they might not have logical address. 82 private final ArraySet<Integer> mCecSwitches = new ArraySet<>(); 83 // Copy of mDeviceInfos to guarantee thread-safety. 84 @GuardedBy("mLock") 85 private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 86 // All external cec input(source) devices. Does not include system audio device. 87 @GuardedBy("mLock") 88 private List<HdmiDeviceInfo> mSafeExternalInputs = Collections.emptyList(); 89 // HDMI port information. Stored in the unmodifiable list to keep the static information 90 // from being modified. 91 @GuardedBy("mLock") 92 private List<HdmiPortInfo> mPortInfo = Collections.emptyList(); 93 94 // Map from path(physical address) to port ID. 95 private UnmodifiableSparseIntArray mPortIdMap; 96 97 // Map from port ID to HdmiPortInfo. 98 private UnmodifiableSparseArray<HdmiPortInfo> mPortInfoMap; 99 100 // Map from port ID to HdmiDeviceInfo. 101 private UnmodifiableSparseArray<HdmiDeviceInfo> mPortDeviceMap; 102 103 // Cached physical address. 104 private int mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS; 105 HdmiCecNetwork(HdmiControlService hdmiControlService, HdmiCecController hdmiCecController, HdmiMhlControllerStub hdmiMhlController)106 HdmiCecNetwork(HdmiControlService hdmiControlService, 107 HdmiCecController hdmiCecController, 108 HdmiMhlControllerStub hdmiMhlController) { 109 mHdmiControlService = hdmiControlService; 110 mHdmiCecController = hdmiCecController; 111 mHdmiMhlController = hdmiMhlController; 112 mHandler = new Handler(mHdmiControlService.getServiceLooper()); 113 mLock = mHdmiControlService.getServiceLock(); 114 } 115 isConnectedToCecSwitch(int path, Collection<Integer> switches)116 private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) { 117 for (int switchPath : switches) { 118 if (isParentPath(switchPath, path)) { 119 return true; 120 } 121 } 122 return false; 123 } 124 isParentPath(int parentPath, int childPath)125 private static boolean isParentPath(int parentPath, int childPath) { 126 // (A000, AB00) (AB00, ABC0), (ABC0, ABCD) 127 // If child's last non-zero nibble is removed, the result equals to the parent. 128 for (int i = 0; i <= 12; i += 4) { 129 int nibble = (childPath >> i) & 0xF; 130 if (nibble != 0) { 131 int parentNibble = (parentPath >> i) & 0xF; 132 return parentNibble == 0 && (childPath >> i + 4) == (parentPath >> i + 4); 133 } 134 } 135 return false; 136 } 137 addLocalDevice(int deviceType, HdmiCecLocalDevice device)138 public void addLocalDevice(int deviceType, HdmiCecLocalDevice device) { 139 mLocalDevices.put(deviceType, device); 140 } 141 142 /** 143 * Return the locally hosted logical device of a given type. 144 * 145 * @param deviceType logical device type 146 * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available; 147 * otherwise null. 148 */ getLocalDevice(int deviceType)149 HdmiCecLocalDevice getLocalDevice(int deviceType) { 150 return mLocalDevices.get(deviceType); 151 } 152 153 /** 154 * Return a list of all {@link HdmiCecLocalDevice}s. 155 * 156 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 157 */ 158 @ServiceThreadOnly getLocalDeviceList()159 List<HdmiCecLocalDevice> getLocalDeviceList() { 160 assertRunOnServiceThread(); 161 return HdmiUtils.sparseArrayToList(mLocalDevices); 162 } 163 164 @ServiceThreadOnly isAllocatedLocalDeviceAddress(int address)165 boolean isAllocatedLocalDeviceAddress(int address) { 166 assertRunOnServiceThread(); 167 for (int i = 0; i < mLocalDevices.size(); ++i) { 168 if (mLocalDevices.valueAt(i).isAddressOf(address)) { 169 return true; 170 } 171 } 172 return false; 173 } 174 175 @ServiceThreadOnly clearLocalDevices()176 void clearLocalDevices() { 177 assertRunOnServiceThread(); 178 mLocalDevices.clear(); 179 } 180 181 /** 182 * Get the device info of a local device or a device in the CEC network by a device id. 183 * @param id id of the device to get 184 * @return the device with the given id, or {@code null} 185 */ 186 @Nullable getDeviceInfo(int id)187 public HdmiDeviceInfo getDeviceInfo(int id) { 188 return mDeviceInfos.get(id); 189 } 190 191 /** 192 * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same 193 * logical address as new device info's. 194 * 195 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 196 * 197 * @param deviceInfo a new {@link HdmiDeviceInfo} to be added. 198 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo} 199 * that has the same logical address as new one has. 200 */ 201 @ServiceThreadOnly addDeviceInfo(HdmiDeviceInfo deviceInfo)202 private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) { 203 assertRunOnServiceThread(); 204 HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress()); 205 mHdmiControlService.checkLogicalAddressConflictAndReallocate( 206 deviceInfo.getLogicalAddress(), deviceInfo.getPhysicalAddress()); 207 if (oldDeviceInfo != null) { 208 removeDeviceInfo(deviceInfo.getId()); 209 } 210 mDeviceInfos.append(deviceInfo.getId(), deviceInfo); 211 updateSafeDeviceInfoList(); 212 return oldDeviceInfo; 213 } 214 215 /** 216 * Remove a device info corresponding to the given {@code logicalAddress}. 217 * It returns removed {@link HdmiDeviceInfo} if exists. 218 * 219 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 220 * 221 * @param id id of device to be removed 222 * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null} 223 */ 224 @ServiceThreadOnly removeDeviceInfo(int id)225 private HdmiDeviceInfo removeDeviceInfo(int id) { 226 assertRunOnServiceThread(); 227 HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id); 228 if (deviceInfo != null) { 229 mDeviceInfos.remove(id); 230 } 231 updateSafeDeviceInfoList(); 232 return deviceInfo; 233 } 234 235 /** 236 * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}. 237 * 238 * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}. 239 * 240 * @param logicalAddress logical address of the device to be retrieved 241 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 242 * Returns null if no logical address matched 243 */ 244 @ServiceThreadOnly 245 @Nullable getCecDeviceInfo(int logicalAddress)246 HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) { 247 assertRunOnServiceThread(); 248 return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress)); 249 } 250 251 /** 252 * Called when a device is newly added or a new device is detected or 253 * existing device is updated. 254 * 255 * @param info device info of a new device. 256 */ 257 @ServiceThreadOnly addCecDevice(HdmiDeviceInfo info)258 final void addCecDevice(HdmiDeviceInfo info) { 259 assertRunOnServiceThread(); 260 HdmiDeviceInfo old = addDeviceInfo(info); 261 if (isLocalDeviceAddress(info.getLogicalAddress())) { 262 // The addition of a local device should not notify listeners 263 return; 264 } 265 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 266 if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 267 // Don't notify listeners of devices that haven't reported their physical address yet 268 return; 269 } else if (old == null || old.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 270 invokeDeviceEventListener(info, 271 HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 272 } else if (!old.equals(info)) { 273 invokeDeviceEventListener(old, 274 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 275 invokeDeviceEventListener(info, 276 HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 277 } 278 } 279 invokeDeviceEventListener(HdmiDeviceInfo info, int event)280 private void invokeDeviceEventListener(HdmiDeviceInfo info, int event) { 281 if (!hideDevicesBehindLegacySwitch(info)) { 282 mHdmiControlService.invokeDeviceEventListeners(info, event); 283 } 284 } 285 286 /** 287 * Called when a device is updated. 288 * 289 * @param info device info of the updating device. 290 */ 291 @ServiceThreadOnly updateCecDevice(HdmiDeviceInfo info)292 final void updateCecDevice(HdmiDeviceInfo info) { 293 assertRunOnServiceThread(); 294 HdmiDeviceInfo old = addDeviceInfo(info); 295 296 if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 297 // Don't notify listeners of devices that haven't reported their physical address yet 298 return; 299 } else if (old == null || old.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 300 invokeDeviceEventListener(info, 301 HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 302 } else if (!old.equals(info)) { 303 invokeDeviceEventListener(info, 304 HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 305 } 306 } 307 308 @ServiceThreadOnly updateSafeDeviceInfoList()309 private void updateSafeDeviceInfoList() { 310 assertRunOnServiceThread(); 311 List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 312 List<HdmiDeviceInfo> externalInputs = getInputDevices(); 313 mSafeAllDeviceInfos = copiedDevices; 314 mSafeExternalInputs = externalInputs; 315 } 316 317 /** 318 * Return a list of all {@link HdmiDeviceInfo}. 319 * 320 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 321 * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which 322 * does not include local device. 323 */ 324 @ServiceThreadOnly getDeviceInfoList(boolean includeLocalDevice)325 List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) { 326 assertRunOnServiceThread(); 327 if (includeLocalDevice) { 328 return HdmiUtils.sparseArrayToList(mDeviceInfos); 329 } else { 330 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 331 for (int i = 0; i < mDeviceInfos.size(); ++i) { 332 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 333 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 334 infoList.add(info); 335 } 336 } 337 return infoList; 338 } 339 } 340 341 /** 342 * Return external input devices. 343 */ 344 @GuardedBy("mLock") getSafeExternalInputsLocked()345 List<HdmiDeviceInfo> getSafeExternalInputsLocked() { 346 return mSafeExternalInputs; 347 } 348 349 /** 350 * Return a list of external cec input (source) devices. 351 * 352 * <p>Note that this effectively excludes non-source devices like system audio, 353 * secondary TV. 354 */ getInputDevices()355 private List<HdmiDeviceInfo> getInputDevices() { 356 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 357 for (int i = 0; i < mDeviceInfos.size(); ++i) { 358 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 359 if (isLocalDeviceAddress(info.getLogicalAddress())) { 360 continue; 361 } 362 if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) { 363 infoList.add(info); 364 } 365 } 366 return infoList; 367 } 368 369 // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch. 370 // This only applies to TV devices. 371 // Returns true if the policy is set to true, and the device to check does not have 372 // a parent CEC device (which should be the CEC-enabled switch) in the list. 373 // Devices with an invalid physical address are assumed to NOT be connected to a legacy switch. hideDevicesBehindLegacySwitch(HdmiDeviceInfo info)374 private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) { 375 return isLocalDeviceAddress(Constants.ADDR_TV) 376 && HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH 377 && !isConnectedToCecSwitch(info.getPhysicalAddress(), getCecSwitches()) 378 && info.getPhysicalAddress() != HdmiDeviceInfo.PATH_INVALID; 379 } 380 381 /** 382 * Called when a device is removed or removal of device is detected. 383 * 384 * @param address a logical address of a device to be removed 385 */ 386 @ServiceThreadOnly removeCecDevice(HdmiCecLocalDevice localDevice, int address)387 final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) { 388 assertRunOnServiceThread(); 389 HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); 390 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 391 localDevice.mCecMessageCache.flushMessagesFrom(address); 392 if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 393 // Don't notify listeners of devices that haven't reported their physical address yet 394 return; 395 } 396 invokeDeviceEventListener(info, 397 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 398 } 399 updateDevicePowerStatus(int logicalAddress, int newPowerStatus)400 public void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 401 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 402 if (info == null) { 403 Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); 404 return; 405 } 406 407 if (info.getDevicePowerStatus() == newPowerStatus) { 408 return; 409 } 410 411 updateCecDevice(info.toBuilder().setDevicePowerStatus(newPowerStatus).build()); 412 } 413 414 /** 415 * Whether a device of the specified physical address is connected to ARC enabled port. 416 */ isConnectedToArcPort(int physicalAddress)417 boolean isConnectedToArcPort(int physicalAddress) { 418 int portId = physicalAddressToPortId(physicalAddress); 419 if (portId != Constants.INVALID_PORT_ID && portId != Constants.CEC_SWITCH_HOME) { 420 return mPortInfoMap.get(portId).isArcSupported(); 421 } 422 return false; 423 } 424 425 426 // Initialize HDMI port information. Combine the information from CEC and MHL HAL and 427 // keep them in one place. 428 @ServiceThreadOnly 429 @VisibleForTesting initPortInfo()430 public void initPortInfo() { 431 assertRunOnServiceThread(); 432 HdmiPortInfo[] cecPortInfo = null; 433 // CEC HAL provides majority of the info while MHL does only MHL support flag for 434 // each port. Return empty array if CEC HAL didn't provide the info. 435 if (mHdmiCecController != null) { 436 cecPortInfo = mHdmiCecController.getPortInfos(); 437 // Invalid cached physical address. 438 mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS; 439 } 440 if (cecPortInfo == null) { 441 return; 442 } 443 444 SparseArray<HdmiPortInfo> portInfoMap = new SparseArray<>(); 445 SparseIntArray portIdMap = new SparseIntArray(); 446 SparseArray<HdmiDeviceInfo> portDeviceMap = new SparseArray<>(); 447 for (HdmiPortInfo info : cecPortInfo) { 448 portIdMap.put(info.getAddress(), info.getId()); 449 portInfoMap.put(info.getId(), info); 450 portDeviceMap.put(info.getId(), 451 HdmiDeviceInfo.hardwarePort(info.getAddress(), info.getId())); 452 } 453 mPortIdMap = new UnmodifiableSparseIntArray(portIdMap); 454 mPortInfoMap = new UnmodifiableSparseArray<>(portInfoMap); 455 mPortDeviceMap = new UnmodifiableSparseArray<>(portDeviceMap); 456 457 if (mHdmiMhlController == null) { 458 return; 459 } 460 HdmiPortInfo[] mhlPortInfo = mHdmiMhlController.getPortInfos(); 461 ArraySet<Integer> mhlSupportedPorts = new ArraySet<Integer>(mhlPortInfo.length); 462 for (HdmiPortInfo info : mhlPortInfo) { 463 if (info.isMhlSupported()) { 464 mhlSupportedPorts.add(info.getId()); 465 } 466 } 467 468 // Build HDMI port info list with CEC port info plus MHL supported flag. We can just use 469 // cec port info if we do not have have port that supports MHL. 470 if (mhlSupportedPorts.isEmpty()) { 471 setPortInfo(Collections.unmodifiableList(Arrays.asList(cecPortInfo))); 472 return; 473 } 474 ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length); 475 for (HdmiPortInfo info : cecPortInfo) { 476 if (mhlSupportedPorts.contains(info.getId())) { 477 result.add(new HdmiPortInfo.Builder(info.getId(), info.getType(), info.getAddress()) 478 .setCecSupported(info.isCecSupported()) 479 .setMhlSupported(true) 480 .setArcSupported(info.isArcSupported()) 481 .setEarcSupported(info.isEarcSupported()) 482 .build()); 483 } else { 484 result.add(info); 485 } 486 } 487 setPortInfo(Collections.unmodifiableList(result)); 488 } 489 getDeviceForPortId(int portId)490 HdmiDeviceInfo getDeviceForPortId(int portId) { 491 return mPortDeviceMap.get(portId, HdmiDeviceInfo.INACTIVE_DEVICE); 492 } 493 494 /** 495 * Whether a device of the specified physical address and logical address exists 496 * in a device info list. However, both are minimal condition and it could 497 * be different device from the original one. 498 * 499 * @param logicalAddress logical address of a device to be searched 500 * @param physicalAddress physical address of a device to be searched 501 * @return true if exist; otherwise false 502 */ 503 @ServiceThreadOnly isInDeviceList(int logicalAddress, int physicalAddress)504 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 505 assertRunOnServiceThread(); 506 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 507 if (device == null) { 508 return false; 509 } 510 return device.getPhysicalAddress() == physicalAddress; 511 } 512 513 /** 514 * Attempts to deduce the device type of a device given its logical address. 515 * If multiple types are possible, returns {@link HdmiDeviceInfo#DEVICE_RESERVED}. 516 */ logicalAddressToDeviceType(int logicalAddress)517 private static int logicalAddressToDeviceType(int logicalAddress) { 518 switch (logicalAddress) { 519 case Constants.ADDR_TV: 520 return HdmiDeviceInfo.DEVICE_TV; 521 case Constants.ADDR_RECORDER_1: 522 case Constants.ADDR_RECORDER_2: 523 case Constants.ADDR_RECORDER_3: 524 return HdmiDeviceInfo.DEVICE_RECORDER; 525 case Constants.ADDR_TUNER_1: 526 case Constants.ADDR_TUNER_2: 527 case Constants.ADDR_TUNER_3: 528 case Constants.ADDR_TUNER_4: 529 return HdmiDeviceInfo.DEVICE_TUNER; 530 case Constants.ADDR_PLAYBACK_1: 531 case Constants.ADDR_PLAYBACK_2: 532 case Constants.ADDR_PLAYBACK_3: 533 return HdmiDeviceInfo.DEVICE_PLAYBACK; 534 case Constants.ADDR_AUDIO_SYSTEM: 535 return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; 536 default: 537 return HdmiDeviceInfo.DEVICE_RESERVED; 538 } 539 } 540 541 /** 542 * Passively listen to incoming CEC messages. 543 * 544 * This shall not result in any CEC messages being sent. 545 */ 546 @ServiceThreadOnly handleCecMessage(HdmiCecMessage message)547 public void handleCecMessage(HdmiCecMessage message) { 548 assertRunOnServiceThread(); 549 // Add device by logical address if it's not already known 550 int sourceAddress = message.getSource(); 551 if (getCecDeviceInfo(sourceAddress) == null) { 552 HdmiDeviceInfo newDevice = HdmiDeviceInfo.cecDeviceBuilder() 553 .setLogicalAddress(sourceAddress) 554 .setDisplayName(HdmiUtils.getDefaultDeviceName(sourceAddress)) 555 .setDeviceType(logicalAddressToDeviceType(sourceAddress)) 556 .build(); 557 addCecDevice(newDevice); 558 } 559 560 // If a message type has its own class, all valid messages of that type 561 // will be represented by an instance of that class. 562 if (message instanceof ReportFeaturesMessage) { 563 handleReportFeatures((ReportFeaturesMessage) message); 564 } 565 566 switch (message.getOpcode()) { 567 case Constants.MESSAGE_FEATURE_ABORT: 568 handleFeatureAbort(message); 569 break; 570 case Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS: 571 handleReportPhysicalAddress(message); 572 break; 573 case Constants.MESSAGE_REPORT_POWER_STATUS: 574 handleReportPowerStatus(message); 575 break; 576 case Constants.MESSAGE_SET_OSD_NAME: 577 handleSetOsdName(message); 578 break; 579 case Constants.MESSAGE_DEVICE_VENDOR_ID: 580 handleDeviceVendorId(message); 581 break; 582 case Constants.MESSAGE_CEC_VERSION: 583 handleCecVersion(message); 584 break; 585 } 586 } 587 588 @ServiceThreadOnly handleReportFeatures(ReportFeaturesMessage message)589 private void handleReportFeatures(ReportFeaturesMessage message) { 590 assertRunOnServiceThread(); 591 592 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 593 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 594 .setCecVersion(message.getCecVersion()) 595 .updateDeviceFeatures(message.getDeviceFeatures()) 596 .build(); 597 598 updateCecDevice(newDeviceInfo); 599 600 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 601 } 602 603 @ServiceThreadOnly handleFeatureAbort(HdmiCecMessage message)604 private void handleFeatureAbort(HdmiCecMessage message) { 605 assertRunOnServiceThread(); 606 607 if (message.getParams().length < 2) { 608 return; 609 } 610 611 int originalOpcode = message.getParams()[0] & 0xFF; 612 int reason = message.getParams()[1] & 0xFF; 613 614 // Check if we received <Feature Abort> in response to <Set Audio Volume Level>. 615 // This provides information on whether the source supports the message. 616 if (originalOpcode == Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL) { 617 618 @DeviceFeatures.FeatureSupportStatus int featureSupport = 619 reason == Constants.ABORT_UNRECOGNIZED_OPCODE 620 ? DeviceFeatures.FEATURE_NOT_SUPPORTED 621 : DeviceFeatures.FEATURE_SUPPORT_UNKNOWN; 622 623 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 624 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 625 .updateDeviceFeatures( 626 currentDeviceInfo.getDeviceFeatures().toBuilder() 627 .setSetAudioVolumeLevelSupport(featureSupport) 628 .build() 629 ) 630 .build(); 631 updateCecDevice(newDeviceInfo); 632 633 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 634 } 635 } 636 637 @ServiceThreadOnly handleCecVersion(HdmiCecMessage message)638 private void handleCecVersion(HdmiCecMessage message) { 639 assertRunOnServiceThread(); 640 641 int version = Byte.toUnsignedInt(message.getParams()[0]); 642 updateDeviceCecVersion(message.getSource(), version); 643 } 644 645 @ServiceThreadOnly handleReportPhysicalAddress(HdmiCecMessage message)646 private void handleReportPhysicalAddress(HdmiCecMessage message) { 647 assertRunOnServiceThread(); 648 int logicalAddress = message.getSource(); 649 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 650 int type = message.getParams()[2]; 651 652 if (updateCecSwitchInfo(logicalAddress, type, physicalAddress)) return; 653 654 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 655 if (deviceInfo == null) { 656 Slog.i(TAG, "Unknown source device info for <Report Physical Address> " + message); 657 } else { 658 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 659 .setPhysicalAddress(physicalAddress) 660 .setPortId(physicalAddressToPortId(physicalAddress)) 661 .setDeviceType(type) 662 .build(); 663 updateCecDevice(updatedDeviceInfo); 664 } 665 } 666 667 @ServiceThreadOnly handleReportPowerStatus(HdmiCecMessage message)668 private void handleReportPowerStatus(HdmiCecMessage message) { 669 assertRunOnServiceThread(); 670 // Update power status of device 671 int newStatus = message.getParams()[0] & 0xFF; 672 updateDevicePowerStatus(message.getSource(), newStatus); 673 674 if (message.getDestination() == Constants.ADDR_BROADCAST) { 675 updateDeviceCecVersion(message.getSource(), HdmiControlManager.HDMI_CEC_VERSION_2_0); 676 } 677 } 678 679 @ServiceThreadOnly updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion)680 private void updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion) { 681 assertRunOnServiceThread(); 682 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 683 if (deviceInfo == null) { 684 Slog.w(TAG, "Can not update CEC version of non-existing device:" + logicalAddress); 685 return; 686 } 687 688 if (deviceInfo.getCecVersion() == hdmiCecVersion) { 689 return; 690 } 691 692 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 693 .setCecVersion(hdmiCecVersion) 694 .build(); 695 updateCecDevice(updatedDeviceInfo); 696 } 697 698 @ServiceThreadOnly handleSetOsdName(HdmiCecMessage message)699 private void handleSetOsdName(HdmiCecMessage message) { 700 assertRunOnServiceThread(); 701 int logicalAddress = message.getSource(); 702 String osdName; 703 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 704 // If the device is not in device list, ignore it. 705 if (deviceInfo == null) { 706 Slog.i(TAG, "No source device info for <Set Osd Name>." + message); 707 return; 708 } 709 try { 710 osdName = new String(message.getParams(), "US-ASCII"); 711 } catch (UnsupportedEncodingException e) { 712 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 713 return; 714 } 715 716 if (deviceInfo.getDisplayName() != null 717 && deviceInfo.getDisplayName().equals(osdName)) { 718 Slog.d(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 719 return; 720 } 721 722 Slog.d(TAG, "Updating device OSD name from " 723 + deviceInfo.getDisplayName() 724 + " to " + osdName); 725 726 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 727 .setDisplayName(osdName) 728 .build(); 729 updateCecDevice(updatedDeviceInfo); 730 } 731 732 @ServiceThreadOnly handleDeviceVendorId(HdmiCecMessage message)733 private void handleDeviceVendorId(HdmiCecMessage message) { 734 assertRunOnServiceThread(); 735 int logicalAddress = message.getSource(); 736 int vendorId = HdmiUtils.threeBytesToInt(message.getParams()); 737 738 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 739 if (deviceInfo == null) { 740 Slog.i(TAG, "Unknown source device info for <Device Vendor ID> " + message); 741 } else { 742 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 743 .setVendorId(vendorId) 744 .build(); 745 updateCecDevice(updatedDeviceInfo); 746 } 747 } 748 addCecSwitch(int physicalAddress)749 void addCecSwitch(int physicalAddress) { 750 mCecSwitches.add(physicalAddress); 751 } 752 getCecSwitches()753 public ArraySet<Integer> getCecSwitches() { 754 return mCecSwitches; 755 } 756 removeCecSwitches(int portId)757 void removeCecSwitches(int portId) { 758 Iterator<Integer> it = mCecSwitches.iterator(); 759 while (it.hasNext()) { 760 int path = it.next(); 761 int devicePortId = physicalAddressToPortId(path); 762 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 763 it.remove(); 764 } 765 } 766 } 767 removeDevicesConnectedToPort(int portId)768 void removeDevicesConnectedToPort(int portId) { 769 removeCecSwitches(portId); 770 771 List<Integer> toRemove = new ArrayList<>(); 772 for (int i = 0; i < mDeviceInfos.size(); i++) { 773 int key = mDeviceInfos.keyAt(i); 774 int physicalAddress = mDeviceInfos.get(key).getPhysicalAddress(); 775 int devicePortId = physicalAddressToPortId(physicalAddress); 776 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 777 toRemove.add(key); 778 } 779 } 780 for (Integer key : toRemove) { 781 removeDeviceInfo(key); 782 } 783 } 784 updateCecSwitchInfo(int address, int type, int path)785 boolean updateCecSwitchInfo(int address, int type, int path) { 786 if (address == Constants.ADDR_UNREGISTERED 787 && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) { 788 mCecSwitches.add(path); 789 updateSafeDeviceInfoList(); 790 return true; // Pure switch does not need further processing. Return here. 791 } 792 if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) { 793 mCecSwitches.add(path); 794 } 795 return false; 796 } 797 798 @GuardedBy("mLock") getSafeCecDevicesLocked()799 List<HdmiDeviceInfo> getSafeCecDevicesLocked() { 800 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 801 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 802 if (isLocalDeviceAddress(info.getLogicalAddress())) { 803 continue; 804 } 805 infoList.add(info); 806 } 807 return infoList; 808 } 809 810 /** 811 * Thread safe version of {@link #getCecDeviceInfo(int)}. 812 * 813 * @param logicalAddress logical address to be retrieved 814 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 815 * Returns null if no logical address matched 816 */ 817 @Nullable getSafeCecDeviceInfo(int logicalAddress)818 HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) { 819 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 820 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) { 821 return info; 822 } 823 } 824 return null; 825 } 826 827 /** 828 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 829 * the given routing path. CEC devices use routing path for its physical address to 830 * describe the hierarchy of the devices in the network. 831 * 832 * @param path routing path or physical address 833 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 834 */ 835 @ServiceThreadOnly getDeviceInfoByPath(int path)836 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 837 assertRunOnServiceThread(); 838 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 839 if (info.getPhysicalAddress() == path) { 840 return info; 841 } 842 } 843 return null; 844 } 845 846 /** 847 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 848 * the given routing path. This is the version accessible safely from threads 849 * other than service thread. 850 * 851 * @param path routing path or physical address 852 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 853 */ getSafeDeviceInfoByPath(int path)854 HdmiDeviceInfo getSafeDeviceInfoByPath(int path) { 855 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 856 if (info.getPhysicalAddress() == path) { 857 return info; 858 } 859 } 860 return null; 861 } 862 getPhysicalAddress()863 public int getPhysicalAddress() { 864 if (mPhysicalAddress == Constants.INVALID_PHYSICAL_ADDRESS) { 865 mPhysicalAddress = mHdmiCecController.getPhysicalAddress(); 866 } 867 return mPhysicalAddress; 868 } 869 870 @ServiceThreadOnly clear()871 public void clear() { 872 assertRunOnServiceThread(); 873 initPortInfo(); 874 clearDeviceList(); 875 clearLocalDevices(); 876 } 877 878 @ServiceThreadOnly removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices)879 void removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices) { 880 ArrayList<Integer> deviceTypesToRemove = new ArrayList<>(); 881 for (int i = 0; i < mLocalDevices.size(); i++) { 882 int deviceType = mLocalDevices.keyAt(i); 883 boolean shouldRemoveLocalDevice = allocatedDevices.stream().noneMatch( 884 localDevice -> localDevice.getDeviceInfo() != null 885 && localDevice.getDeviceInfo().getDeviceType() == deviceType); 886 if (shouldRemoveLocalDevice) { 887 deviceTypesToRemove.add(deviceType); 888 } 889 } 890 for (Integer deviceType : deviceTypesToRemove) { 891 mLocalDevices.remove(deviceType); 892 } 893 } 894 895 @ServiceThreadOnly removeLocalDeviceWithType(int deviceType)896 void removeLocalDeviceWithType(int deviceType) { 897 mLocalDevices.remove(deviceType); 898 } 899 900 @ServiceThreadOnly clearDeviceList()901 public void clearDeviceList() { 902 assertRunOnServiceThread(); 903 for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) { 904 if (info.getPhysicalAddress() == getPhysicalAddress() 905 || info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 906 // Don't notify listeners of local devices or devices that haven't reported their 907 // physical address yet 908 continue; 909 } 910 invokeDeviceEventListener(info, 911 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 912 } 913 mDeviceInfos.clear(); 914 updateSafeDeviceInfoList(); 915 } 916 917 /** 918 * Returns HDMI port information for the given port id. 919 * 920 * @param portId HDMI port id 921 * @return {@link HdmiPortInfo} for the given port 922 */ getPortInfo(int portId)923 HdmiPortInfo getPortInfo(int portId) { 924 return mPortInfoMap.get(portId, null); 925 } 926 927 /** 928 * Returns the routing path (physical address) of the HDMI port for the given 929 * port id. 930 */ portIdToPath(int portId)931 int portIdToPath(int portId) { 932 if (portId == Constants.CEC_SWITCH_HOME) { 933 return getPhysicalAddress(); 934 } 935 HdmiPortInfo portInfo = getPortInfo(portId); 936 if (portInfo == null) { 937 Slog.e(TAG, "Cannot find the port info: " + portId); 938 return Constants.INVALID_PHYSICAL_ADDRESS; 939 } 940 return portInfo.getAddress(); 941 } 942 943 /** 944 * Returns the id of HDMI port located at the current device that runs this method. 945 * 946 * For TV with physical address 0x0000, target device 0x1120, we want port physical address 947 * 0x1000 to get the correct port id from {@link #mPortIdMap}. For device with Physical Address 948 * 0x2000, target device 0x2420, we want port address 0x24000 to get the port id. 949 * 950 * <p>Return {@link Constants#INVALID_PORT_ID} if target device does not connect to. 951 * 952 * @param path the target device's physical address. 953 * @return the id of the port that the target device eventually connects to 954 * on the current device. 955 */ physicalAddressToPortId(int path)956 int physicalAddressToPortId(int path) { 957 int physicalAddress = getPhysicalAddress(); 958 if (path == physicalAddress) { 959 // The local device isn't connected to any port; assign portId 0 960 return Constants.CEC_SWITCH_HOME; 961 } 962 int mask = 0xF000; 963 int finalMask = 0xF000; 964 int maskedAddress = physicalAddress; 965 966 while (maskedAddress != 0) { 967 maskedAddress = physicalAddress & mask; 968 finalMask |= mask; 969 mask >>= 4; 970 } 971 972 int portAddress = path & finalMask; 973 return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID); 974 } 975 getPortInfo()976 List<HdmiPortInfo> getPortInfo() { 977 return mPortInfo; 978 } 979 setPortInfo(List<HdmiPortInfo> portInfo)980 void setPortInfo(List<HdmiPortInfo> portInfo) { 981 mPortInfo = portInfo; 982 } 983 isLocalDeviceAddress(int address)984 private boolean isLocalDeviceAddress(int address) { 985 for (int i = 0; i < mLocalDevices.size(); i++) { 986 int key = mLocalDevices.keyAt(i); 987 if (mLocalDevices.get(key).getDeviceInfo().getLogicalAddress() == address) { 988 return true; 989 } 990 } 991 return false; 992 } 993 assertRunOnServiceThread()994 private void assertRunOnServiceThread() { 995 if (Looper.myLooper() != mHandler.getLooper()) { 996 throw new IllegalStateException("Should run on service thread."); 997 } 998 } 999 dump(IndentingPrintWriter pw)1000 protected void dump(IndentingPrintWriter pw) { 1001 pw.println("HDMI CEC Network"); 1002 pw.increaseIndent(); 1003 HdmiUtils.dumpIterable(pw, "mPortInfo:", mPortInfo); 1004 for (int i = 0; i < mLocalDevices.size(); ++i) { 1005 pw.println("HdmiCecLocalDevice #" + mLocalDevices.keyAt(i) + ":"); 1006 pw.increaseIndent(); 1007 mLocalDevices.valueAt(i).dump(pw); 1008 1009 pw.println("Active Source history:"); 1010 pw.increaseIndent(); 1011 final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 1012 ArrayBlockingQueue<HdmiCecController.Dumpable> activeSourceHistory = 1013 mLocalDevices.valueAt(i).getActiveSourceHistory(); 1014 for (HdmiCecController.Dumpable activeSourceEvent : activeSourceHistory) { 1015 activeSourceEvent.dump(pw, sdf); 1016 } 1017 pw.decreaseIndent(); 1018 pw.decreaseIndent(); 1019 } 1020 HdmiUtils.dumpIterable(pw, "mDeviceInfos:", mSafeAllDeviceInfos); 1021 pw.decreaseIndent(); 1022 } 1023 } 1024