• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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