1 /*
2  * Copyright (C) 2014 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 package com.android.server.hdmi;
17 
18 import android.hardware.hdmi.HdmiDeviceInfo;
19 import android.util.Slog;
20 
21 import com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource;
22 
23 import java.io.UnsupportedEncodingException;
24 
25 /**
26  * Feature action that discovers the information of a newly found logical device.
27  *
28  * This action is created when receiving <Report Physical Address>, a CEC command a newly
29  * connected HDMI-CEC device broadcasts to announce its advent. Additional commands are issued in
30  * this action to gather more information on the device such as OSD name and device vendor ID.
31  *
32  * <p>The result is made in the form of {@link HdmiDeviceInfo} object, and passed to service
33  * for the management through its life cycle.
34  *
35  * <p>Package-private, accessed by {@link HdmiControlService} only.
36  */
37 final class NewDeviceAction extends HdmiCecFeatureAction {
38 
39     private static final String TAG = "NewDeviceAction";
40 
41     // State in which the action sent <Give OSD Name> and is waiting for <Set OSD Name>
42     // that contains the name of the device for display on screen.
43     static final int STATE_WAITING_FOR_SET_OSD_NAME = 1;
44 
45     // State in which the action sent <Give Device Vendor ID> and is waiting for
46     // <Device Vendor ID> that contains the vendor ID of the device.
47     static final int STATE_WAITING_FOR_DEVICE_VENDOR_ID = 2;
48 
49     private final int mDeviceLogicalAddress;
50     private final int mDevicePhysicalAddress;
51     private final int mDeviceType;
52 
53     private int mVendorId;
54     private String mDisplayName;
55     private int mTimeoutRetry;
56     private HdmiDeviceInfo mOldDeviceInfo;
57 
58     /**
59      * Constructor.
60      *
61      * @param source {@link HdmiCecLocalDevice} instance
62      * @param deviceLogicalAddress logical address of the device in interest
63      * @param devicePhysicalAddress physical address of the device in interest
64      * @param deviceType type of the device
65      */
NewDeviceAction(HdmiCecLocalDevice source, int deviceLogicalAddress, int devicePhysicalAddress, int deviceType)66     NewDeviceAction(HdmiCecLocalDevice source, int deviceLogicalAddress,
67             int devicePhysicalAddress, int deviceType) {
68         super(source);
69         mDeviceLogicalAddress = deviceLogicalAddress;
70         mDevicePhysicalAddress = devicePhysicalAddress;
71         mDeviceType = deviceType;
72         mVendorId = Constants.VENDOR_ID_UNKNOWN;
73     }
74 
75     @Override
start()76     public boolean start() {
77         mOldDeviceInfo =
78             localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress);
79         // If there's deviceInfo with same (logical address, physical address) set
80         // Then addCecDevice should be delayed until system information process is finished
81         if (mOldDeviceInfo != null
82                 && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) {
83             Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:["
84                     + mOldDeviceInfo.toString() + "]");
85         } else {
86             // Add the device ahead with default information to handle <Active Source>
87             // promptly, rather than waiting till the new device action is finished.
88             Slog.d(TAG, "Start NewDeviceAction with default deviceInfo");
89             HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder()
90                     .setLogicalAddress(mDeviceLogicalAddress)
91                     .setPhysicalAddress(mDevicePhysicalAddress)
92                     .setPortId(tv().getPortId(mDevicePhysicalAddress))
93                     .setDeviceType(mDeviceType)
94                     .setVendorId(Constants.VENDOR_ID_UNKNOWN)
95                     .build();
96             // If a deviceInfo with same logical address but different physical address exists
97             // We should remove the old deviceInfo first
98             // This will happen if the interval between unplugging and plugging device is too short
99             // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly
100             // plugged device violates HDMI Spec and uses an occupied logical address
101             if (mOldDeviceInfo != null) {
102                 Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: "
103                         + mDevicePhysicalAddress);
104                 localDevice().mService.getHdmiCecNetwork().removeCecDevice(
105                         localDevice(), mDeviceLogicalAddress);
106             }
107             localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
108         }
109         requestOsdName(true);
110         return true;
111     }
112 
requestOsdName(boolean firstTry)113     private void requestOsdName(boolean firstTry) {
114         if (firstTry) {
115             mTimeoutRetry = 0;
116         }
117         mState = STATE_WAITING_FOR_SET_OSD_NAME;
118         if (mayProcessCommandIfCached(mDeviceLogicalAddress, Constants.MESSAGE_SET_OSD_NAME)) {
119             return;
120         }
121 
122         sendCommand(HdmiCecMessageBuilder.buildGiveOsdNameCommand(getSourceAddress(),
123                 mDeviceLogicalAddress));
124         addTimer(mState, HdmiConfig.TIMEOUT_MS);
125     }
126 
127     @Override
processCommand(HdmiCecMessage cmd)128     public boolean processCommand(HdmiCecMessage cmd) {
129         // For the logical device in interest, we want two more pieces of information -
130         // osd name and vendor id. They are requested in sequence. In case we don't
131         // get the expected responses (either by timeout or by receiving <feature abort> command),
132         // set them to a default osd name and unknown vendor id respectively.
133         int opcode = cmd.getOpcode();
134         int src = cmd.getSource();
135         byte[] params = cmd.getParams();
136 
137         if (mDeviceLogicalAddress != src) {
138             return false;
139         }
140 
141         if (mState == STATE_WAITING_FOR_SET_OSD_NAME) {
142             if (opcode == Constants.MESSAGE_SET_OSD_NAME) {
143                 try {
144                     mDisplayName = new String(params, "US-ASCII");
145                 } catch (UnsupportedEncodingException e) {
146                     Slog.e(TAG, "Failed to get OSD name: " + e.getMessage());
147                 }
148                 requestVendorId(true);
149                 return true;
150             } else if (opcode == Constants.MESSAGE_FEATURE_ABORT) {
151                 int requestOpcode = params[0] & 0xFF;
152                 if (requestOpcode == Constants.MESSAGE_GIVE_OSD_NAME) {
153                     requestVendorId(true);
154                     return true;
155                 }
156             }
157         } else if (mState == STATE_WAITING_FOR_DEVICE_VENDOR_ID) {
158             if (opcode == Constants.MESSAGE_DEVICE_VENDOR_ID) {
159                 mVendorId = HdmiUtils.threeBytesToInt(params);
160                 addDeviceInfo();
161                 finish();
162                 return true;
163             } else if (opcode == Constants.MESSAGE_FEATURE_ABORT) {
164                 int requestOpcode = params[0] & 0xFF;
165                 if (requestOpcode == Constants.MESSAGE_GIVE_DEVICE_VENDOR_ID) {
166                     addDeviceInfo();
167                     finish();
168                     return true;
169                 }
170             }
171         }
172         return false;
173     }
174 
mayProcessCommandIfCached(int destAddress, int opcode)175     private boolean mayProcessCommandIfCached(int destAddress, int opcode) {
176         HdmiCecMessage message = getCecMessageCache().getMessage(destAddress, opcode);
177         if (message != null) {
178             return processCommand(message);
179         }
180         return false;
181     }
182 
requestVendorId(boolean firstTry)183     private void requestVendorId(boolean firstTry) {
184         if (firstTry) {
185             mTimeoutRetry = 0;
186         }
187         // At first, transit to waiting status for <Device Vendor Id>.
188         mState = STATE_WAITING_FOR_DEVICE_VENDOR_ID;
189         // If the message is already in cache, process it.
190         if (mayProcessCommandIfCached(mDeviceLogicalAddress,
191                 Constants.MESSAGE_DEVICE_VENDOR_ID)) {
192             return;
193         }
194         sendCommand(HdmiCecMessageBuilder.buildGiveDeviceVendorIdCommand(getSourceAddress(),
195                 mDeviceLogicalAddress));
196         addTimer(mState, HdmiConfig.TIMEOUT_MS);
197     }
198 
addDeviceInfo()199     private void addDeviceInfo() {
200         // The device should be in the device list with default information.
201         if (!localDevice().mService.getHdmiCecNetwork().isInDeviceList(mDeviceLogicalAddress,
202                 mDevicePhysicalAddress)) {
203             Slog.w(TAG, String.format("Device not found (%02x, %04x)",
204                     mDeviceLogicalAddress, mDevicePhysicalAddress));
205             return;
206         }
207         if (mDisplayName == null) {
208             mDisplayName = "";
209         }
210         HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder()
211                 .setLogicalAddress(mDeviceLogicalAddress)
212                 .setPhysicalAddress(mDevicePhysicalAddress)
213                 .setPortId(tv().getPortId(mDevicePhysicalAddress))
214                 .setDeviceType(mDeviceType)
215                 .setVendorId(mVendorId)
216                 .setDisplayName(mDisplayName)
217                 .build();
218 
219         // Check if oldDevice is same as newDevice
220         // If so, don't add newDevice info, preventing ARC or HDMI source re-connection
221         if (mOldDeviceInfo != null
222                 && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress
223                 && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress
224                 && mOldDeviceInfo.getDeviceType() == mDeviceType
225                 && mOldDeviceInfo.getVendorId() == mVendorId
226                 && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) {
227             // Consume CEC messages we already got for this newly found device.
228             tv().processDelayedMessages(mDeviceLogicalAddress);
229             Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device");
230             Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString()
231                     + "]; New:[" + deviceInfo.toString() + "]");
232         } else {
233             Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]");
234             localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo);
235 
236             // Consume CEC messages we already got for this newly found device.
237             tv().processDelayedMessages(mDeviceLogicalAddress);
238             if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM,
239                         mDeviceLogicalAddress)) {
240                 tv().onNewAvrAdded(deviceInfo);
241             }
242         }
243     }
244 
245     @Override
handleTimerEvent(int state)246     public void handleTimerEvent(int state) {
247         if (mState == STATE_NONE || mState != state) {
248             return;
249         }
250         if (state == STATE_WAITING_FOR_SET_OSD_NAME) {
251             if (++mTimeoutRetry < HdmiConfig.TIMEOUT_RETRY) {
252                 requestOsdName(false);
253                 return;
254             }
255             // Osd name request timed out. Try vendor id
256             requestVendorId(true);
257         } else if (state == STATE_WAITING_FOR_DEVICE_VENDOR_ID) {
258             if (++mTimeoutRetry < HdmiConfig.TIMEOUT_RETRY) {
259                 requestVendorId(false);
260                 return;
261             }
262             // vendor id timed out. Go ahead creating the device info what we've got so far.
263             addDeviceInfo();
264             finish();
265         }
266     }
267 
isActionOf(ActiveSource activeSource)268     boolean isActionOf(ActiveSource activeSource) {
269         return (mDeviceLogicalAddress == activeSource.logicalAddress)
270                 && (mDevicePhysicalAddress == activeSource.physicalAddress);
271     }
272 }
273