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