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