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