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