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 (event == HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE || 282 !hideDevicesBehindLegacySwitch(info)) { 283 mHdmiControlService.invokeDeviceEventListeners(info, event); 284 } 285 } 286 287 /** 288 * Called when a device is updated. 289 * 290 * @param info device info of the updating device. 291 */ 292 @ServiceThreadOnly updateCecDevice(HdmiDeviceInfo info)293 final void updateCecDevice(HdmiDeviceInfo info) { 294 assertRunOnServiceThread(); 295 HdmiDeviceInfo old = addDeviceInfo(info); 296 297 if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 298 // Don't notify listeners of devices that haven't reported their physical address yet 299 return; 300 } else if (old == null || old.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 301 invokeDeviceEventListener(info, 302 HdmiControlManager.DEVICE_EVENT_ADD_DEVICE); 303 } else if (!old.equals(info)) { 304 invokeDeviceEventListener(info, 305 HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE); 306 } 307 } 308 309 @ServiceThreadOnly updateSafeDeviceInfoList()310 private void updateSafeDeviceInfoList() { 311 assertRunOnServiceThread(); 312 List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 313 List<HdmiDeviceInfo> externalInputs = getInputDevices(); 314 mSafeAllDeviceInfos = copiedDevices; 315 mSafeExternalInputs = externalInputs; 316 } 317 318 /** 319 * Return a list of all {@link HdmiDeviceInfo}. 320 * 321 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 322 * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which 323 * does not include local device. 324 */ 325 @ServiceThreadOnly getDeviceInfoList(boolean includeLocalDevice)326 List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) { 327 assertRunOnServiceThread(); 328 if (includeLocalDevice) { 329 return HdmiUtils.sparseArrayToList(mDeviceInfos); 330 } else { 331 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 332 for (int i = 0; i < mDeviceInfos.size(); ++i) { 333 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 334 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 335 infoList.add(info); 336 } 337 } 338 return infoList; 339 } 340 } 341 342 /** 343 * Return external input devices. 344 */ 345 @GuardedBy("mLock") getSafeExternalInputsLocked()346 List<HdmiDeviceInfo> getSafeExternalInputsLocked() { 347 return mSafeExternalInputs; 348 } 349 350 /** 351 * Return a list of external cec input (source) devices. 352 * 353 * <p>Note that this effectively excludes non-source devices like system audio, 354 * secondary TV. 355 */ getInputDevices()356 private List<HdmiDeviceInfo> getInputDevices() { 357 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 358 for (int i = 0; i < mDeviceInfos.size(); ++i) { 359 HdmiDeviceInfo info = mDeviceInfos.valueAt(i); 360 if (isLocalDeviceAddress(info.getLogicalAddress())) { 361 continue; 362 } 363 if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) { 364 infoList.add(info); 365 } 366 } 367 return infoList; 368 } 369 370 // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch. 371 // This only applies to TV devices. 372 // Returns true if the policy is set to true, and the device to check does not have 373 // a parent CEC device (which should be the CEC-enabled switch) in the list. 374 // Devices with an invalid physical address are assumed to NOT be connected to a legacy switch. hideDevicesBehindLegacySwitch(HdmiDeviceInfo info)375 private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) { 376 return isLocalDeviceAddress(Constants.ADDR_TV) 377 && HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH 378 && !isConnectedToCecSwitch(info.getPhysicalAddress(), getCecSwitches()) 379 && info.getPhysicalAddress() != HdmiDeviceInfo.PATH_INVALID; 380 } 381 382 /** 383 * Called when a device is removed or removal of device is detected. 384 * 385 * @param address a logical address of a device to be removed 386 */ 387 @ServiceThreadOnly removeCecDevice(HdmiCecLocalDevice localDevice, int address)388 final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) { 389 assertRunOnServiceThread(); 390 HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address)); 391 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 392 localDevice.mCecMessageCache.flushMessagesFrom(address); 393 if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 394 // Don't notify listeners of devices that haven't reported their physical address yet 395 return; 396 } 397 invokeDeviceEventListener(info, 398 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 399 } 400 updateDevicePowerStatus(int logicalAddress, int newPowerStatus)401 public void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) { 402 HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress); 403 if (info == null) { 404 Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress); 405 return; 406 } 407 408 if (info.getDevicePowerStatus() == newPowerStatus) { 409 return; 410 } 411 412 updateCecDevice(info.toBuilder().setDevicePowerStatus(newPowerStatus).build()); 413 } 414 415 /** 416 * Whether a device of the specified physical address is connected to ARC enabled port. 417 */ isConnectedToArcPort(int physicalAddress)418 boolean isConnectedToArcPort(int physicalAddress) { 419 int portId = physicalAddressToPortId(physicalAddress); 420 if (portId != Constants.INVALID_PORT_ID && portId != Constants.CEC_SWITCH_HOME) { 421 return mPortInfoMap.get(portId).isArcSupported(); 422 } 423 return false; 424 } 425 426 427 // Initialize HDMI port information. Combine the information from CEC and MHL HAL and 428 // keep them in one place. 429 @ServiceThreadOnly 430 @VisibleForTesting initPortInfo()431 public void initPortInfo() { 432 assertRunOnServiceThread(); 433 HdmiPortInfo[] cecPortInfo = null; 434 // CEC HAL provides majority of the info while MHL does only MHL support flag for 435 // each port. Return empty array if CEC HAL didn't provide the info. 436 if (mHdmiCecController != null) { 437 cecPortInfo = mHdmiCecController.getPortInfos(); 438 // Invalid cached physical address. 439 mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS; 440 } 441 if (cecPortInfo == null) { 442 return; 443 } 444 445 SparseArray<HdmiPortInfo> portInfoMap = new SparseArray<>(); 446 SparseIntArray portIdMap = new SparseIntArray(); 447 SparseArray<HdmiDeviceInfo> portDeviceMap = new SparseArray<>(); 448 for (HdmiPortInfo info : cecPortInfo) { 449 portIdMap.put(info.getAddress(), info.getId()); 450 portInfoMap.put(info.getId(), info); 451 portDeviceMap.put(info.getId(), 452 HdmiDeviceInfo.hardwarePort(info.getAddress(), info.getId())); 453 } 454 mPortIdMap = new UnmodifiableSparseIntArray(portIdMap); 455 mPortInfoMap = new UnmodifiableSparseArray<>(portInfoMap); 456 mPortDeviceMap = new UnmodifiableSparseArray<>(portDeviceMap); 457 458 if (mHdmiMhlController == null) { 459 return; 460 } 461 HdmiPortInfo[] mhlPortInfo = mHdmiMhlController.getPortInfos(); 462 ArraySet<Integer> mhlSupportedPorts = new ArraySet<Integer>(mhlPortInfo.length); 463 for (HdmiPortInfo info : mhlPortInfo) { 464 if (info.isMhlSupported()) { 465 mhlSupportedPorts.add(info.getId()); 466 } 467 } 468 469 // Build HDMI port info list with CEC port info plus MHL supported flag. We can just use 470 // cec port info if we do not have have port that supports MHL. 471 if (mhlSupportedPorts.isEmpty()) { 472 setPortInfo(Collections.unmodifiableList(Arrays.asList(cecPortInfo))); 473 return; 474 } 475 ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length); 476 for (HdmiPortInfo info : cecPortInfo) { 477 if (mhlSupportedPorts.contains(info.getId())) { 478 result.add(new HdmiPortInfo.Builder(info.getId(), info.getType(), info.getAddress()) 479 .setCecSupported(info.isCecSupported()) 480 .setMhlSupported(true) 481 .setArcSupported(info.isArcSupported()) 482 .setEarcSupported(info.isEarcSupported()) 483 .build()); 484 } else { 485 result.add(info); 486 } 487 } 488 setPortInfo(Collections.unmodifiableList(result)); 489 } 490 getDeviceForPortId(int portId)491 HdmiDeviceInfo getDeviceForPortId(int portId) { 492 return mPortDeviceMap.get(portId, HdmiDeviceInfo.INACTIVE_DEVICE); 493 } 494 495 /** 496 * Whether a device of the specified physical address and logical address exists 497 * in a device info list. However, both are minimal condition and it could 498 * be different device from the original one. 499 * 500 * @param logicalAddress logical address of a device to be searched 501 * @param physicalAddress physical address of a device to be searched 502 * @return true if exist; otherwise false 503 */ 504 @ServiceThreadOnly isInDeviceList(int logicalAddress, int physicalAddress)505 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 506 assertRunOnServiceThread(); 507 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 508 if (device == null) { 509 return false; 510 } 511 return device.getPhysicalAddress() == physicalAddress; 512 } 513 514 /** 515 * Attempts to deduce the device type of a device given its logical address. 516 * If multiple types are possible, returns {@link HdmiDeviceInfo#DEVICE_RESERVED}. 517 */ logicalAddressToDeviceType(int logicalAddress)518 private static int logicalAddressToDeviceType(int logicalAddress) { 519 switch (logicalAddress) { 520 case Constants.ADDR_TV: 521 return HdmiDeviceInfo.DEVICE_TV; 522 case Constants.ADDR_RECORDER_1: 523 case Constants.ADDR_RECORDER_2: 524 case Constants.ADDR_RECORDER_3: 525 return HdmiDeviceInfo.DEVICE_RECORDER; 526 case Constants.ADDR_TUNER_1: 527 case Constants.ADDR_TUNER_2: 528 case Constants.ADDR_TUNER_3: 529 case Constants.ADDR_TUNER_4: 530 return HdmiDeviceInfo.DEVICE_TUNER; 531 case Constants.ADDR_PLAYBACK_1: 532 case Constants.ADDR_PLAYBACK_2: 533 case Constants.ADDR_PLAYBACK_3: 534 return HdmiDeviceInfo.DEVICE_PLAYBACK; 535 case Constants.ADDR_AUDIO_SYSTEM: 536 return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; 537 default: 538 return HdmiDeviceInfo.DEVICE_RESERVED; 539 } 540 } 541 542 /** 543 * Passively listen to incoming CEC messages. 544 * 545 * This shall not result in any CEC messages being sent. 546 */ 547 @ServiceThreadOnly handleCecMessage(HdmiCecMessage message)548 public void handleCecMessage(HdmiCecMessage message) { 549 assertRunOnServiceThread(); 550 // Add device by logical address if it's not already known 551 int sourceAddress = message.getSource(); 552 if (getCecDeviceInfo(sourceAddress) == null) { 553 HdmiDeviceInfo newDevice = HdmiDeviceInfo.cecDeviceBuilder() 554 .setLogicalAddress(sourceAddress) 555 .setDisplayName(HdmiUtils.getDefaultDeviceName(sourceAddress)) 556 .setDeviceType(logicalAddressToDeviceType(sourceAddress)) 557 .build(); 558 addCecDevice(newDevice); 559 } 560 561 // If a message type has its own class, all valid messages of that type 562 // will be represented by an instance of that class. 563 if (message instanceof ReportFeaturesMessage) { 564 handleReportFeatures((ReportFeaturesMessage) message); 565 } 566 567 switch (message.getOpcode()) { 568 case Constants.MESSAGE_FEATURE_ABORT: 569 handleFeatureAbort(message); 570 break; 571 case Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS: 572 handleReportPhysicalAddress(message); 573 break; 574 case Constants.MESSAGE_REPORT_POWER_STATUS: 575 handleReportPowerStatus(message); 576 break; 577 case Constants.MESSAGE_SET_OSD_NAME: 578 handleSetOsdName(message); 579 break; 580 case Constants.MESSAGE_DEVICE_VENDOR_ID: 581 handleDeviceVendorId(message); 582 break; 583 case Constants.MESSAGE_CEC_VERSION: 584 handleCecVersion(message); 585 break; 586 } 587 } 588 589 @ServiceThreadOnly handleReportFeatures(ReportFeaturesMessage message)590 private void handleReportFeatures(ReportFeaturesMessage message) { 591 assertRunOnServiceThread(); 592 593 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 594 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 595 .setCecVersion(message.getCecVersion()) 596 .updateDeviceFeatures(message.getDeviceFeatures()) 597 .build(); 598 599 updateCecDevice(newDeviceInfo); 600 601 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 602 } 603 604 @ServiceThreadOnly handleFeatureAbort(HdmiCecMessage message)605 private void handleFeatureAbort(HdmiCecMessage message) { 606 assertRunOnServiceThread(); 607 608 if (message.getParams().length < 2) { 609 return; 610 } 611 612 int originalOpcode = message.getParams()[0] & 0xFF; 613 int reason = message.getParams()[1] & 0xFF; 614 615 // Check if we received <Feature Abort> in response to <Set Audio Volume Level>. 616 // This provides information on whether the source supports the message. 617 if (originalOpcode == Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL) { 618 619 @DeviceFeatures.FeatureSupportStatus int featureSupport = 620 reason == Constants.ABORT_UNRECOGNIZED_OPCODE 621 ? DeviceFeatures.FEATURE_NOT_SUPPORTED 622 : DeviceFeatures.FEATURE_SUPPORT_UNKNOWN; 623 624 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 625 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 626 .updateDeviceFeatures( 627 currentDeviceInfo.getDeviceFeatures().toBuilder() 628 .setSetAudioVolumeLevelSupport(featureSupport) 629 .build() 630 ) 631 .build(); 632 updateCecDevice(newDeviceInfo); 633 634 mHdmiControlService.checkAndUpdateAbsoluteVolumeBehavior(); 635 } 636 } 637 638 @ServiceThreadOnly handleCecVersion(HdmiCecMessage message)639 private void handleCecVersion(HdmiCecMessage message) { 640 assertRunOnServiceThread(); 641 642 int version = Byte.toUnsignedInt(message.getParams()[0]); 643 updateDeviceCecVersion(message.getSource(), version); 644 } 645 646 @ServiceThreadOnly handleReportPhysicalAddress(HdmiCecMessage message)647 private void handleReportPhysicalAddress(HdmiCecMessage message) { 648 assertRunOnServiceThread(); 649 int logicalAddress = message.getSource(); 650 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 651 int type = message.getParams()[2]; 652 653 if (updateCecSwitchInfo(logicalAddress, type, physicalAddress)) return; 654 655 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 656 if (deviceInfo == null) { 657 Slog.i(TAG, "Unknown source device info for <Report Physical Address> " + message); 658 } else { 659 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 660 .setPhysicalAddress(physicalAddress) 661 .setPortId(physicalAddressToPortId(physicalAddress)) 662 .setDeviceType(type) 663 .build(); 664 if (deviceInfo.getPhysicalAddress() != physicalAddress) { 665 addCecDevice(updatedDeviceInfo); 666 } else { 667 updateCecDevice(updatedDeviceInfo); 668 } 669 } 670 } 671 672 @ServiceThreadOnly handleReportPowerStatus(HdmiCecMessage message)673 private void handleReportPowerStatus(HdmiCecMessage message) { 674 assertRunOnServiceThread(); 675 // Update power status of device 676 int newStatus = message.getParams()[0] & 0xFF; 677 updateDevicePowerStatus(message.getSource(), newStatus); 678 679 if (message.getDestination() == Constants.ADDR_BROADCAST) { 680 updateDeviceCecVersion(message.getSource(), HdmiControlManager.HDMI_CEC_VERSION_2_0); 681 } 682 } 683 684 @ServiceThreadOnly updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion)685 private void updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion) { 686 assertRunOnServiceThread(); 687 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 688 if (deviceInfo == null) { 689 Slog.w(TAG, "Can not update CEC version of non-existing device:" + logicalAddress); 690 return; 691 } 692 693 if (deviceInfo.getCecVersion() == hdmiCecVersion) { 694 return; 695 } 696 697 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 698 .setCecVersion(hdmiCecVersion) 699 .build(); 700 updateCecDevice(updatedDeviceInfo); 701 } 702 703 @ServiceThreadOnly handleSetOsdName(HdmiCecMessage message)704 private void handleSetOsdName(HdmiCecMessage message) { 705 assertRunOnServiceThread(); 706 int logicalAddress = message.getSource(); 707 String osdName; 708 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 709 // If the device is not in device list, ignore it. 710 if (deviceInfo == null) { 711 Slog.i(TAG, "No source device info for <Set Osd Name>." + message); 712 return; 713 } 714 try { 715 osdName = new String(message.getParams(), "US-ASCII"); 716 } catch (UnsupportedEncodingException e) { 717 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 718 return; 719 } 720 721 if (deviceInfo.getDisplayName() != null 722 && deviceInfo.getDisplayName().equals(osdName)) { 723 Slog.d(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 724 return; 725 } 726 727 Slog.d(TAG, "Updating device OSD name from " 728 + deviceInfo.getDisplayName() 729 + " to " + osdName); 730 731 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 732 .setDisplayName(osdName) 733 .build(); 734 updateCecDevice(updatedDeviceInfo); 735 } 736 737 @ServiceThreadOnly handleDeviceVendorId(HdmiCecMessage message)738 private void handleDeviceVendorId(HdmiCecMessage message) { 739 assertRunOnServiceThread(); 740 int logicalAddress = message.getSource(); 741 int vendorId = HdmiUtils.threeBytesToInt(message.getParams()); 742 743 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 744 if (deviceInfo == null) { 745 Slog.i(TAG, "Unknown source device info for <Device Vendor ID> " + message); 746 } else { 747 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 748 .setVendorId(vendorId) 749 .build(); 750 updateCecDevice(updatedDeviceInfo); 751 } 752 } 753 addCecSwitch(int physicalAddress)754 void addCecSwitch(int physicalAddress) { 755 mCecSwitches.add(physicalAddress); 756 } 757 getCecSwitches()758 public ArraySet<Integer> getCecSwitches() { 759 return mCecSwitches; 760 } 761 removeCecSwitches(int portId)762 void removeCecSwitches(int portId) { 763 Iterator<Integer> it = mCecSwitches.iterator(); 764 while (it.hasNext()) { 765 int path = it.next(); 766 int devicePortId = physicalAddressToPortId(path); 767 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 768 it.remove(); 769 } 770 } 771 } 772 removeDevicesConnectedToPort(int portId)773 void removeDevicesConnectedToPort(int portId) { 774 removeCecSwitches(portId); 775 776 List<Integer> toRemove = new ArrayList<>(); 777 for (int i = 0; i < mDeviceInfos.size(); i++) { 778 int key = mDeviceInfos.keyAt(i); 779 int physicalAddress = mDeviceInfos.get(key).getPhysicalAddress(); 780 int devicePortId = physicalAddressToPortId(physicalAddress); 781 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 782 toRemove.add(key); 783 } 784 } 785 for (Integer key : toRemove) { 786 removeDeviceInfo(key); 787 } 788 } 789 updateCecSwitchInfo(int address, int type, int path)790 boolean updateCecSwitchInfo(int address, int type, int path) { 791 if (address == Constants.ADDR_UNREGISTERED 792 && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) { 793 mCecSwitches.add(path); 794 updateSafeDeviceInfoList(); 795 return true; // Pure switch does not need further processing. Return here. 796 } 797 if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) { 798 mCecSwitches.add(path); 799 } 800 return false; 801 } 802 803 @GuardedBy("mLock") getSafeCecDevicesLocked()804 List<HdmiDeviceInfo> getSafeCecDevicesLocked() { 805 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 806 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 807 if (isLocalDeviceAddress(info.getLogicalAddress())) { 808 continue; 809 } 810 infoList.add(info); 811 } 812 return infoList; 813 } 814 815 /** 816 * Thread safe version of {@link #getCecDeviceInfo(int)}. 817 * 818 * @param logicalAddress logical address to be retrieved 819 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 820 * Returns null if no logical address matched 821 */ 822 @Nullable getSafeCecDeviceInfo(int logicalAddress)823 HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) { 824 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 825 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) { 826 return info; 827 } 828 } 829 return null; 830 } 831 832 /** 833 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 834 * the given routing path. CEC devices use routing path for its physical address to 835 * describe the hierarchy of the devices in the network. 836 * 837 * @param path routing path or physical address 838 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 839 */ 840 @ServiceThreadOnly getDeviceInfoByPath(int path)841 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 842 assertRunOnServiceThread(); 843 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 844 if (info.getPhysicalAddress() == path) { 845 return info; 846 } 847 } 848 return null; 849 } 850 851 /** 852 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 853 * the given routing path. This is the version accessible safely from threads 854 * other than service thread. 855 * 856 * @param path routing path or physical address 857 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 858 */ getSafeDeviceInfoByPath(int path)859 HdmiDeviceInfo getSafeDeviceInfoByPath(int path) { 860 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 861 if (info.getPhysicalAddress() == path) { 862 return info; 863 } 864 } 865 return null; 866 } 867 getPhysicalAddress()868 public int getPhysicalAddress() { 869 if (mPhysicalAddress == Constants.INVALID_PHYSICAL_ADDRESS) { 870 mPhysicalAddress = mHdmiCecController.getPhysicalAddress(); 871 } 872 return mPhysicalAddress; 873 } 874 875 @ServiceThreadOnly clear()876 public void clear() { 877 assertRunOnServiceThread(); 878 initPortInfo(); 879 clearDeviceList(); 880 clearLocalDevices(); 881 } 882 883 @ServiceThreadOnly removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices)884 void removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices) { 885 ArrayList<Integer> deviceTypesToRemove = new ArrayList<>(); 886 for (int i = 0; i < mLocalDevices.size(); i++) { 887 int deviceType = mLocalDevices.keyAt(i); 888 boolean shouldRemoveLocalDevice = allocatedDevices.stream().noneMatch( 889 localDevice -> localDevice.getDeviceInfo() != null 890 && localDevice.getDeviceInfo().getDeviceType() == deviceType); 891 if (shouldRemoveLocalDevice) { 892 deviceTypesToRemove.add(deviceType); 893 } 894 } 895 for (Integer deviceType : deviceTypesToRemove) { 896 mLocalDevices.remove(deviceType); 897 } 898 } 899 900 @ServiceThreadOnly removeLocalDeviceWithType(int deviceType)901 void removeLocalDeviceWithType(int deviceType) { 902 mLocalDevices.remove(deviceType); 903 } 904 905 @ServiceThreadOnly clearDeviceList()906 public void clearDeviceList() { 907 assertRunOnServiceThread(); 908 for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) { 909 if (info.getPhysicalAddress() == getPhysicalAddress() 910 || info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 911 // Don't notify listeners of local devices or devices that haven't reported their 912 // physical address yet 913 continue; 914 } 915 invokeDeviceEventListener(info, 916 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 917 } 918 mDeviceInfos.clear(); 919 updateSafeDeviceInfoList(); 920 } 921 922 /** 923 * Returns HDMI port information for the given port id. 924 * 925 * @param portId HDMI port id 926 * @return {@link HdmiPortInfo} for the given port 927 */ getPortInfo(int portId)928 HdmiPortInfo getPortInfo(int portId) { 929 return mPortInfoMap.get(portId, null); 930 } 931 932 /** 933 * Returns the routing path (physical address) of the HDMI port for the given 934 * port id. 935 */ portIdToPath(int portId)936 int portIdToPath(int portId) { 937 if (portId == Constants.CEC_SWITCH_HOME) { 938 return getPhysicalAddress(); 939 } 940 HdmiPortInfo portInfo = getPortInfo(portId); 941 if (portInfo == null) { 942 Slog.e(TAG, "Cannot find the port info: " + portId); 943 return Constants.INVALID_PHYSICAL_ADDRESS; 944 } 945 return portInfo.getAddress(); 946 } 947 948 /** 949 * Returns the id of HDMI port located at the current device that runs this method. 950 * 951 * For TV with physical address 0x0000, target device 0x1120, we want port physical address 952 * 0x1000 to get the correct port id from {@link #mPortIdMap}. For device with Physical Address 953 * 0x2000, target device 0x2420, we want port address 0x24000 to get the port id. 954 * 955 * <p>Return {@link Constants#INVALID_PORT_ID} if target device does not connect to. 956 * 957 * @param path the target device's physical address. 958 * @return the id of the port that the target device eventually connects to 959 * on the current device. 960 */ physicalAddressToPortId(int path)961 int physicalAddressToPortId(int path) { 962 int physicalAddress = getPhysicalAddress(); 963 if (path == physicalAddress) { 964 // The local device isn't connected to any port; assign portId 0 965 return Constants.CEC_SWITCH_HOME; 966 } 967 int mask = 0xF000; 968 int finalMask = 0xF000; 969 int maskedAddress = physicalAddress; 970 971 while (maskedAddress != 0) { 972 maskedAddress = physicalAddress & mask; 973 finalMask |= mask; 974 mask >>= 4; 975 } 976 977 int portAddress = path & finalMask; 978 return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID); 979 } 980 getPortInfo()981 List<HdmiPortInfo> getPortInfo() { 982 return mPortInfo; 983 } 984 setPortInfo(List<HdmiPortInfo> portInfo)985 void setPortInfo(List<HdmiPortInfo> portInfo) { 986 mPortInfo = portInfo; 987 } 988 isLocalDeviceAddress(int address)989 private boolean isLocalDeviceAddress(int address) { 990 for (int i = 0; i < mLocalDevices.size(); i++) { 991 int key = mLocalDevices.keyAt(i); 992 if (mLocalDevices.get(key).getDeviceInfo().getLogicalAddress() == address) { 993 return true; 994 } 995 } 996 return false; 997 } 998 assertRunOnServiceThread()999 private void assertRunOnServiceThread() { 1000 if (Looper.myLooper() != mHandler.getLooper()) { 1001 throw new IllegalStateException("Should run on service thread."); 1002 } 1003 } 1004 dump(IndentingPrintWriter pw)1005 protected void dump(IndentingPrintWriter pw) { 1006 pw.println("HDMI CEC Network"); 1007 pw.increaseIndent(); 1008 HdmiUtils.dumpIterable(pw, "mPortInfo:", mPortInfo); 1009 for (int i = 0; i < mLocalDevices.size(); ++i) { 1010 pw.println("HdmiCecLocalDevice #" + mLocalDevices.keyAt(i) + ":"); 1011 pw.increaseIndent(); 1012 mLocalDevices.valueAt(i).dump(pw); 1013 1014 pw.println("Active Source history:"); 1015 pw.increaseIndent(); 1016 final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 1017 ArrayBlockingQueue<HdmiCecController.Dumpable> activeSourceHistory = 1018 mLocalDevices.valueAt(i).getActiveSourceHistory(); 1019 for (HdmiCecController.Dumpable activeSourceEvent : activeSourceHistory) { 1020 activeSourceEvent.dump(pw, sdf); 1021 } 1022 pw.decreaseIndent(); 1023 pw.decreaseIndent(); 1024 } 1025 HdmiUtils.dumpIterable(pw, "mDeviceInfos:", mSafeAllDeviceInfos); 1026 pw.decreaseIndent(); 1027 } 1028 } 1029