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.checkAndUpdateAbsoluteVolumeControlState(); 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.checkAndUpdateAbsoluteVolumeControlState(); 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(info.getId(), info.getType(), info.getAddress(), 473 info.isCecSupported(), true, info.isArcSupported())); 474 } else { 475 result.add(info); 476 } 477 } 478 setPortInfo(Collections.unmodifiableList(result)); 479 } 480 getDeviceForPortId(int portId)481 HdmiDeviceInfo getDeviceForPortId(int portId) { 482 return mPortDeviceMap.get(portId, HdmiDeviceInfo.INACTIVE_DEVICE); 483 } 484 485 /** 486 * Whether a device of the specified physical address and logical address exists 487 * in a device info list. However, both are minimal condition and it could 488 * be different device from the original one. 489 * 490 * @param logicalAddress logical address of a device to be searched 491 * @param physicalAddress physical address of a device to be searched 492 * @return true if exist; otherwise false 493 */ 494 @ServiceThreadOnly isInDeviceList(int logicalAddress, int physicalAddress)495 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 496 assertRunOnServiceThread(); 497 HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress); 498 if (device == null) { 499 return false; 500 } 501 return device.getPhysicalAddress() == physicalAddress; 502 } 503 504 /** 505 * Attempts to deduce the device type of a device given its logical address. 506 * If multiple types are possible, returns {@link HdmiDeviceInfo#DEVICE_RESERVED}. 507 */ logicalAddressToDeviceType(int logicalAddress)508 private static int logicalAddressToDeviceType(int logicalAddress) { 509 switch (logicalAddress) { 510 case Constants.ADDR_TV: 511 return HdmiDeviceInfo.DEVICE_TV; 512 case Constants.ADDR_RECORDER_1: 513 case Constants.ADDR_RECORDER_2: 514 case Constants.ADDR_RECORDER_3: 515 return HdmiDeviceInfo.DEVICE_RECORDER; 516 case Constants.ADDR_TUNER_1: 517 case Constants.ADDR_TUNER_2: 518 case Constants.ADDR_TUNER_3: 519 case Constants.ADDR_TUNER_4: 520 return HdmiDeviceInfo.DEVICE_TUNER; 521 case Constants.ADDR_PLAYBACK_1: 522 case Constants.ADDR_PLAYBACK_2: 523 case Constants.ADDR_PLAYBACK_3: 524 return HdmiDeviceInfo.DEVICE_PLAYBACK; 525 case Constants.ADDR_AUDIO_SYSTEM: 526 return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; 527 default: 528 return HdmiDeviceInfo.DEVICE_RESERVED; 529 } 530 } 531 532 /** 533 * Passively listen to incoming CEC messages. 534 * 535 * This shall not result in any CEC messages being sent. 536 */ 537 @ServiceThreadOnly handleCecMessage(HdmiCecMessage message)538 public void handleCecMessage(HdmiCecMessage message) { 539 assertRunOnServiceThread(); 540 // Add device by logical address if it's not already known 541 int sourceAddress = message.getSource(); 542 if (getCecDeviceInfo(sourceAddress) == null) { 543 HdmiDeviceInfo newDevice = HdmiDeviceInfo.cecDeviceBuilder() 544 .setLogicalAddress(sourceAddress) 545 .setDisplayName(HdmiUtils.getDefaultDeviceName(sourceAddress)) 546 .setDeviceType(logicalAddressToDeviceType(sourceAddress)) 547 .build(); 548 addCecDevice(newDevice); 549 } 550 551 // If a message type has its own class, all valid messages of that type 552 // will be represented by an instance of that class. 553 if (message instanceof ReportFeaturesMessage) { 554 handleReportFeatures((ReportFeaturesMessage) message); 555 } 556 557 switch (message.getOpcode()) { 558 case Constants.MESSAGE_FEATURE_ABORT: 559 handleFeatureAbort(message); 560 break; 561 case Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS: 562 handleReportPhysicalAddress(message); 563 break; 564 case Constants.MESSAGE_REPORT_POWER_STATUS: 565 handleReportPowerStatus(message); 566 break; 567 case Constants.MESSAGE_SET_OSD_NAME: 568 handleSetOsdName(message); 569 break; 570 case Constants.MESSAGE_DEVICE_VENDOR_ID: 571 handleDeviceVendorId(message); 572 break; 573 case Constants.MESSAGE_CEC_VERSION: 574 handleCecVersion(message); 575 break; 576 } 577 } 578 579 @ServiceThreadOnly handleReportFeatures(ReportFeaturesMessage message)580 private void handleReportFeatures(ReportFeaturesMessage message) { 581 assertRunOnServiceThread(); 582 583 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 584 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 585 .setCecVersion(message.getCecVersion()) 586 .updateDeviceFeatures(message.getDeviceFeatures()) 587 .build(); 588 589 updateCecDevice(newDeviceInfo); 590 591 mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); 592 } 593 594 @ServiceThreadOnly handleFeatureAbort(HdmiCecMessage message)595 private void handleFeatureAbort(HdmiCecMessage message) { 596 assertRunOnServiceThread(); 597 598 if (message.getParams().length < 2) { 599 return; 600 } 601 602 int originalOpcode = message.getParams()[0] & 0xFF; 603 int reason = message.getParams()[1] & 0xFF; 604 605 // Check if we received <Feature Abort> in response to <Set Audio Volume Level>. 606 // This provides information on whether the source supports the message. 607 if (originalOpcode == Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL) { 608 609 @DeviceFeatures.FeatureSupportStatus int featureSupport = 610 reason == Constants.ABORT_UNRECOGNIZED_OPCODE 611 ? DeviceFeatures.FEATURE_NOT_SUPPORTED 612 : DeviceFeatures.FEATURE_SUPPORT_UNKNOWN; 613 614 HdmiDeviceInfo currentDeviceInfo = getCecDeviceInfo(message.getSource()); 615 HdmiDeviceInfo newDeviceInfo = currentDeviceInfo.toBuilder() 616 .updateDeviceFeatures( 617 currentDeviceInfo.getDeviceFeatures().toBuilder() 618 .setSetAudioVolumeLevelSupport(featureSupport) 619 .build() 620 ) 621 .build(); 622 updateCecDevice(newDeviceInfo); 623 624 mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState(); 625 } 626 } 627 628 @ServiceThreadOnly handleCecVersion(HdmiCecMessage message)629 private void handleCecVersion(HdmiCecMessage message) { 630 assertRunOnServiceThread(); 631 632 int version = Byte.toUnsignedInt(message.getParams()[0]); 633 updateDeviceCecVersion(message.getSource(), version); 634 } 635 636 @ServiceThreadOnly handleReportPhysicalAddress(HdmiCecMessage message)637 private void handleReportPhysicalAddress(HdmiCecMessage message) { 638 assertRunOnServiceThread(); 639 int logicalAddress = message.getSource(); 640 int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams()); 641 int type = message.getParams()[2]; 642 643 if (updateCecSwitchInfo(logicalAddress, type, physicalAddress)) return; 644 645 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 646 if (deviceInfo == null) { 647 Slog.i(TAG, "Unknown source device info for <Report Physical Address> " + message); 648 } else { 649 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 650 .setPhysicalAddress(physicalAddress) 651 .setPortId(physicalAddressToPortId(physicalAddress)) 652 .setDeviceType(type) 653 .build(); 654 updateCecDevice(updatedDeviceInfo); 655 } 656 } 657 658 @ServiceThreadOnly handleReportPowerStatus(HdmiCecMessage message)659 private void handleReportPowerStatus(HdmiCecMessage message) { 660 assertRunOnServiceThread(); 661 // Update power status of device 662 int newStatus = message.getParams()[0] & 0xFF; 663 updateDevicePowerStatus(message.getSource(), newStatus); 664 665 if (message.getDestination() == Constants.ADDR_BROADCAST) { 666 updateDeviceCecVersion(message.getSource(), HdmiControlManager.HDMI_CEC_VERSION_2_0); 667 } 668 } 669 670 @ServiceThreadOnly updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion)671 private void updateDeviceCecVersion(int logicalAddress, int hdmiCecVersion) { 672 assertRunOnServiceThread(); 673 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 674 if (deviceInfo == null) { 675 Slog.w(TAG, "Can not update CEC version of non-existing device:" + logicalAddress); 676 return; 677 } 678 679 if (deviceInfo.getCecVersion() == hdmiCecVersion) { 680 return; 681 } 682 683 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 684 .setCecVersion(hdmiCecVersion) 685 .build(); 686 updateCecDevice(updatedDeviceInfo); 687 } 688 689 @ServiceThreadOnly handleSetOsdName(HdmiCecMessage message)690 private void handleSetOsdName(HdmiCecMessage message) { 691 assertRunOnServiceThread(); 692 int logicalAddress = message.getSource(); 693 String osdName; 694 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 695 // If the device is not in device list, ignore it. 696 if (deviceInfo == null) { 697 Slog.i(TAG, "No source device info for <Set Osd Name>." + message); 698 return; 699 } 700 try { 701 osdName = new String(message.getParams(), "US-ASCII"); 702 } catch (UnsupportedEncodingException e) { 703 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 704 return; 705 } 706 707 if (deviceInfo.getDisplayName() != null 708 && deviceInfo.getDisplayName().equals(osdName)) { 709 Slog.d(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 710 return; 711 } 712 713 Slog.d(TAG, "Updating device OSD name from " 714 + deviceInfo.getDisplayName() 715 + " to " + osdName); 716 717 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 718 .setDisplayName(osdName) 719 .build(); 720 updateCecDevice(updatedDeviceInfo); 721 } 722 723 @ServiceThreadOnly handleDeviceVendorId(HdmiCecMessage message)724 private void handleDeviceVendorId(HdmiCecMessage message) { 725 assertRunOnServiceThread(); 726 int logicalAddress = message.getSource(); 727 int vendorId = HdmiUtils.threeBytesToInt(message.getParams()); 728 729 HdmiDeviceInfo deviceInfo = getCecDeviceInfo(logicalAddress); 730 if (deviceInfo == null) { 731 Slog.i(TAG, "Unknown source device info for <Device Vendor ID> " + message); 732 } else { 733 HdmiDeviceInfo updatedDeviceInfo = deviceInfo.toBuilder() 734 .setVendorId(vendorId) 735 .build(); 736 updateCecDevice(updatedDeviceInfo); 737 } 738 } 739 addCecSwitch(int physicalAddress)740 void addCecSwitch(int physicalAddress) { 741 mCecSwitches.add(physicalAddress); 742 } 743 getCecSwitches()744 public ArraySet<Integer> getCecSwitches() { 745 return mCecSwitches; 746 } 747 removeCecSwitches(int portId)748 void removeCecSwitches(int portId) { 749 Iterator<Integer> it = mCecSwitches.iterator(); 750 while (it.hasNext()) { 751 int path = it.next(); 752 int devicePortId = physicalAddressToPortId(path); 753 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 754 it.remove(); 755 } 756 } 757 } 758 removeDevicesConnectedToPort(int portId)759 void removeDevicesConnectedToPort(int portId) { 760 removeCecSwitches(portId); 761 762 List<Integer> toRemove = new ArrayList<>(); 763 for (int i = 0; i < mDeviceInfos.size(); i++) { 764 int key = mDeviceInfos.keyAt(i); 765 int physicalAddress = mDeviceInfos.get(key).getPhysicalAddress(); 766 int devicePortId = physicalAddressToPortId(physicalAddress); 767 if (devicePortId == portId || devicePortId == Constants.INVALID_PORT_ID) { 768 toRemove.add(key); 769 } 770 } 771 for (Integer key : toRemove) { 772 removeDeviceInfo(key); 773 } 774 } 775 updateCecSwitchInfo(int address, int type, int path)776 boolean updateCecSwitchInfo(int address, int type, int path) { 777 if (address == Constants.ADDR_UNREGISTERED 778 && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) { 779 mCecSwitches.add(path); 780 updateSafeDeviceInfoList(); 781 return true; // Pure switch does not need further processing. Return here. 782 } 783 if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) { 784 mCecSwitches.add(path); 785 } 786 return false; 787 } 788 789 @GuardedBy("mLock") getSafeCecDevicesLocked()790 List<HdmiDeviceInfo> getSafeCecDevicesLocked() { 791 ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>(); 792 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 793 if (isLocalDeviceAddress(info.getLogicalAddress())) { 794 continue; 795 } 796 infoList.add(info); 797 } 798 return infoList; 799 } 800 801 /** 802 * Thread safe version of {@link #getCecDeviceInfo(int)}. 803 * 804 * @param logicalAddress logical address to be retrieved 805 * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}. 806 * Returns null if no logical address matched 807 */ 808 @Nullable getSafeCecDeviceInfo(int logicalAddress)809 HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) { 810 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 811 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) { 812 return info; 813 } 814 } 815 return null; 816 } 817 818 /** 819 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 820 * the given routing path. CEC devices use routing path for its physical address to 821 * describe the hierarchy of the devices in the network. 822 * 823 * @param path routing path or physical address 824 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 825 */ 826 @ServiceThreadOnly getDeviceInfoByPath(int path)827 final HdmiDeviceInfo getDeviceInfoByPath(int path) { 828 assertRunOnServiceThread(); 829 for (HdmiDeviceInfo info : getDeviceInfoList(false)) { 830 if (info.getPhysicalAddress() == path) { 831 return info; 832 } 833 } 834 return null; 835 } 836 837 /** 838 * Returns the {@link HdmiDeviceInfo} instance whose physical address matches 839 * the given routing path. This is the version accessible safely from threads 840 * other than service thread. 841 * 842 * @param path routing path or physical address 843 * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null 844 */ getSafeDeviceInfoByPath(int path)845 HdmiDeviceInfo getSafeDeviceInfoByPath(int path) { 846 for (HdmiDeviceInfo info : mSafeAllDeviceInfos) { 847 if (info.getPhysicalAddress() == path) { 848 return info; 849 } 850 } 851 return null; 852 } 853 getPhysicalAddress()854 public int getPhysicalAddress() { 855 return mHdmiCecController.getPhysicalAddress(); 856 } 857 858 @ServiceThreadOnly clear()859 public void clear() { 860 assertRunOnServiceThread(); 861 initPortInfo(); 862 clearDeviceList(); 863 clearLocalDevices(); 864 } 865 866 @ServiceThreadOnly clearDeviceList()867 public void clearDeviceList() { 868 assertRunOnServiceThread(); 869 for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) { 870 if (info.getPhysicalAddress() == getPhysicalAddress() 871 || info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) { 872 // Don't notify listeners of local devices or devices that haven't reported their 873 // physical address yet 874 continue; 875 } 876 invokeDeviceEventListener(info, 877 HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE); 878 } 879 mDeviceInfos.clear(); 880 updateSafeDeviceInfoList(); 881 } 882 883 /** 884 * Returns HDMI port information for the given port id. 885 * 886 * @param portId HDMI port id 887 * @return {@link HdmiPortInfo} for the given port 888 */ getPortInfo(int portId)889 HdmiPortInfo getPortInfo(int portId) { 890 return mPortInfoMap.get(portId, null); 891 } 892 893 /** 894 * Returns the routing path (physical address) of the HDMI port for the given 895 * port id. 896 */ portIdToPath(int portId)897 int portIdToPath(int portId) { 898 HdmiPortInfo portInfo = getPortInfo(portId); 899 if (portInfo == null) { 900 Slog.e(TAG, "Cannot find the port info: " + portId); 901 return Constants.INVALID_PHYSICAL_ADDRESS; 902 } 903 return portInfo.getAddress(); 904 } 905 906 /** 907 * Returns the id of HDMI port located at the current device that runs this method. 908 * 909 * For TV with physical address 0x0000, target device 0x1120, we want port physical address 910 * 0x1000 to get the correct port id from {@link #mPortIdMap}. For device with Physical Address 911 * 0x2000, target device 0x2420, we want port address 0x24000 to get the port id. 912 * 913 * <p>Return {@link Constants#INVALID_PORT_ID} if target device does not connect to. 914 * 915 * @param path the target device's physical address. 916 * @return the id of the port that the target device eventually connects to 917 * on the current device. 918 */ physicalAddressToPortId(int path)919 int physicalAddressToPortId(int path) { 920 int physicalAddress = getPhysicalAddress(); 921 if (path == physicalAddress) { 922 // The local device isn't connected to any port; assign portId 0 923 return Constants.CEC_SWITCH_HOME; 924 } 925 int mask = 0xF000; 926 int finalMask = 0xF000; 927 int maskedAddress = physicalAddress; 928 929 while (maskedAddress != 0) { 930 maskedAddress = physicalAddress & mask; 931 finalMask |= mask; 932 mask >>= 4; 933 } 934 935 int portAddress = path & finalMask; 936 return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID); 937 } 938 getPortInfo()939 List<HdmiPortInfo> getPortInfo() { 940 return mPortInfo; 941 } 942 setPortInfo(List<HdmiPortInfo> portInfo)943 void setPortInfo(List<HdmiPortInfo> portInfo) { 944 mPortInfo = portInfo; 945 } 946 isLocalDeviceAddress(int address)947 private boolean isLocalDeviceAddress(int address) { 948 for (int i = 0; i < mLocalDevices.size(); i++) { 949 int key = mLocalDevices.keyAt(i); 950 if (mLocalDevices.get(key).getDeviceInfo().getLogicalAddress() == address) { 951 return true; 952 } 953 } 954 return false; 955 } 956 assertRunOnServiceThread()957 private void assertRunOnServiceThread() { 958 if (Looper.myLooper() != mHandler.getLooper()) { 959 throw new IllegalStateException("Should run on service thread."); 960 } 961 } 962 dump(IndentingPrintWriter pw)963 protected void dump(IndentingPrintWriter pw) { 964 pw.println("HDMI CEC Network"); 965 pw.increaseIndent(); 966 HdmiUtils.dumpIterable(pw, "mPortInfo:", mPortInfo); 967 for (int i = 0; i < mLocalDevices.size(); ++i) { 968 pw.println("HdmiCecLocalDevice #" + mLocalDevices.keyAt(i) + ":"); 969 pw.increaseIndent(); 970 mLocalDevices.valueAt(i).dump(pw); 971 972 pw.println("Active Source history:"); 973 pw.increaseIndent(); 974 final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 975 ArrayBlockingQueue<HdmiCecController.Dumpable> activeSourceHistory = 976 mLocalDevices.valueAt(i).getActiveSourceHistory(); 977 for (HdmiCecController.Dumpable activeSourceEvent : activeSourceHistory) { 978 activeSourceEvent.dump(pw, sdf); 979 } 980 pw.decreaseIndent(); 981 pw.decreaseIndent(); 982 } 983 HdmiUtils.dumpIterable(pw, "mDeviceInfos:", mSafeAllDeviceInfos); 984 pw.decreaseIndent(); 985 } 986 } 987