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
17package com.android.server.hdmi;
18
19import android.hardware.hdmi.HdmiDeviceInfo;
20import android.util.Slog;
21
22import com.android.internal.util.Preconditions;
23import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
24
25import java.io.UnsupportedEncodingException;
26import java.util.ArrayList;
27import java.util.List;
28
29/**
30 * Feature action that handles device discovery sequences.
31 * Device discovery is launched when TV device is woken from "Standby" state
32 * or enabled "Control for Hdmi" from disabled state.
33 *
34 * <p>Device discovery goes through the following steps.
35 * <ol>
36 *   <li>Poll all non-local devices by sending &lt;Polling Message&gt;
37 *   <li>Gather "Physical address" and "device type" of all acknowledged devices
38 *   <li>Gather "OSD (display) name" of all acknowledge devices
39 *   <li>Gather "Vendor id" of all acknowledge devices
40 * </ol>
41 * We attempt to get OSD name/vendor ID up to 5 times in case the communication fails.
42 */
43final class DeviceDiscoveryAction extends HdmiCecFeatureAction {
44    private static final String TAG = "DeviceDiscoveryAction";
45
46    // State in which the action is waiting for device polling.
47    private static final int STATE_WAITING_FOR_DEVICE_POLLING = 1;
48    // State in which the action is waiting for gathering physical address of non-local devices.
49    private static final int STATE_WAITING_FOR_PHYSICAL_ADDRESS = 2;
50    // State in which the action is waiting for gathering osd name of non-local devices.
51    private static final int STATE_WAITING_FOR_OSD_NAME = 3;
52    // State in which the action is waiting for gathering vendor id of non-local devices.
53    private static final int STATE_WAITING_FOR_VENDOR_ID = 4;
54
55    /**
56     * Interface used to report result of device discovery.
57     */
58    interface DeviceDiscoveryCallback {
59        /**
60         * Called when device discovery is done.
61         *
62         * @param deviceInfos a list of all non-local devices. It can be empty list.
63         */
64        void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos);
65    }
66
67    // An internal container used to keep track of device information during
68    // this action.
69    private static final class DeviceInfo {
70        private final int mLogicalAddress;
71
72        private int mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS;
73        private int mPortId = Constants.INVALID_PORT_ID;
74        private int mVendorId = Constants.UNKNOWN_VENDOR_ID;
75        private String mDisplayName = "";
76        private int mDeviceType = HdmiDeviceInfo.DEVICE_INACTIVE;
77
78        private DeviceInfo(int logicalAddress) {
79            mLogicalAddress = logicalAddress;
80        }
81
82        private HdmiDeviceInfo toHdmiDeviceInfo() {
83            return new HdmiDeviceInfo(mLogicalAddress, mPhysicalAddress, mPortId, mDeviceType,
84                    mVendorId, mDisplayName);
85        }
86    }
87
88    private final ArrayList<DeviceInfo> mDevices = new ArrayList<>();
89    private final DeviceDiscoveryCallback mCallback;
90    private int mProcessedDeviceCount = 0;
91    private int mTimeoutRetry = 0;
92
93    /**
94     * Constructor.
95     *
96     * @param source an instance of {@link HdmiCecLocalDevice}.
97     */
98    DeviceDiscoveryAction(HdmiCecLocalDevice source, DeviceDiscoveryCallback callback) {
99        super(source);
100        mCallback = Preconditions.checkNotNull(callback);
101    }
102
103    @Override
104    boolean start() {
105        mDevices.clear();
106        mState = STATE_WAITING_FOR_DEVICE_POLLING;
107
108        pollDevices(new DevicePollingCallback() {
109            @Override
110            public void onPollingFinished(List<Integer> ackedAddress) {
111                if (ackedAddress.isEmpty()) {
112                    Slog.v(TAG, "No device is detected.");
113                    wrapUpAndFinish();
114                    return;
115                }
116
117                Slog.v(TAG, "Device detected: " + ackedAddress);
118                allocateDevices(ackedAddress);
119                startPhysicalAddressStage();
120            }
121        }, Constants.POLL_ITERATION_REVERSE_ORDER
122            | Constants.POLL_STRATEGY_REMOTES_DEVICES, HdmiConfig.DEVICE_POLLING_RETRY);
123        return true;
124    }
125
126    private void allocateDevices(List<Integer> addresses) {
127        for (Integer i : addresses) {
128            DeviceInfo info = new DeviceInfo(i);
129            mDevices.add(info);
130        }
131    }
132
133    private void startPhysicalAddressStage() {
134        Slog.v(TAG, "Start [Physical Address Stage]:" + mDevices.size());
135        mProcessedDeviceCount = 0;
136        mState = STATE_WAITING_FOR_PHYSICAL_ADDRESS;
137
138        checkAndProceedStage();
139    }
140
141    private boolean verifyValidLogicalAddress(int address) {
142        return address >= Constants.ADDR_TV && address < Constants.ADDR_UNREGISTERED;
143    }
144
145    private void queryPhysicalAddress(int address) {
146        if (!verifyValidLogicalAddress(address)) {
147            checkAndProceedStage();
148            return;
149        }
150
151        mActionTimer.clearTimerMessage();
152
153        // Check cache first and send request if not exist.
154        if (mayProcessMessageIfCached(address, Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS)) {
155            return;
156        }
157        sendCommand(HdmiCecMessageBuilder.buildGivePhysicalAddress(getSourceAddress(), address));
158        addTimer(mState, HdmiConfig.TIMEOUT_MS);
159    }
160
161    private void startOsdNameStage() {
162        Slog.v(TAG, "Start [Osd Name Stage]:" + mDevices.size());
163        mProcessedDeviceCount = 0;
164        mState = STATE_WAITING_FOR_OSD_NAME;
165
166        checkAndProceedStage();
167    }
168
169    private void queryOsdName(int address) {
170        if (!verifyValidLogicalAddress(address)) {
171            checkAndProceedStage();
172            return;
173        }
174
175        mActionTimer.clearTimerMessage();
176
177        if (mayProcessMessageIfCached(address, Constants.MESSAGE_SET_OSD_NAME)) {
178            return;
179        }
180        sendCommand(HdmiCecMessageBuilder.buildGiveOsdNameCommand(getSourceAddress(), address));
181        addTimer(mState, HdmiConfig.TIMEOUT_MS);
182    }
183
184    private void startVendorIdStage() {
185        Slog.v(TAG, "Start [Vendor Id Stage]:" + mDevices.size());
186
187        mProcessedDeviceCount = 0;
188        mState = STATE_WAITING_FOR_VENDOR_ID;
189
190        checkAndProceedStage();
191    }
192
193    private void queryVendorId(int address) {
194        if (!verifyValidLogicalAddress(address)) {
195            checkAndProceedStage();
196            return;
197        }
198
199        mActionTimer.clearTimerMessage();
200
201        if (mayProcessMessageIfCached(address, Constants.MESSAGE_DEVICE_VENDOR_ID)) {
202            return;
203        }
204        sendCommand(
205                HdmiCecMessageBuilder.buildGiveDeviceVendorIdCommand(getSourceAddress(), address));
206        addTimer(mState, HdmiConfig.TIMEOUT_MS);
207    }
208
209    private boolean mayProcessMessageIfCached(int address, int opcode) {
210        HdmiCecMessage message = getCecMessageCache().getMessage(address, opcode);
211        if (message != null) {
212            processCommand(message);
213            return true;
214        }
215        return false;
216    }
217
218    @Override
219    boolean processCommand(HdmiCecMessage cmd) {
220        switch (mState) {
221            case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
222                if (cmd.getOpcode() == Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS) {
223                    handleReportPhysicalAddress(cmd);
224                    return true;
225                }
226                return false;
227            case STATE_WAITING_FOR_OSD_NAME:
228                if (cmd.getOpcode() == Constants.MESSAGE_SET_OSD_NAME) {
229                    handleSetOsdName(cmd);
230                    return true;
231                }
232                return false;
233            case STATE_WAITING_FOR_VENDOR_ID:
234                if (cmd.getOpcode() == Constants.MESSAGE_DEVICE_VENDOR_ID) {
235                    handleVendorId(cmd);
236                    return true;
237                }
238                return false;
239            case STATE_WAITING_FOR_DEVICE_POLLING:
240                // Fall through.
241            default:
242                return false;
243        }
244    }
245
246    private void handleReportPhysicalAddress(HdmiCecMessage cmd) {
247        Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
248
249        DeviceInfo current = mDevices.get(mProcessedDeviceCount);
250        if (current.mLogicalAddress != cmd.getSource()) {
251            Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
252                    cmd.getSource());
253            return;
254        }
255
256        byte params[] = cmd.getParams();
257        current.mPhysicalAddress = HdmiUtils.twoBytesToInt(params);
258        current.mPortId = getPortId(current.mPhysicalAddress);
259        current.mDeviceType = params[2] & 0xFF;
260
261        tv().updateCecSwitchInfo(current.mLogicalAddress, current.mDeviceType,
262                    current.mPhysicalAddress);
263
264        increaseProcessedDeviceCount();
265        checkAndProceedStage();
266    }
267
268    private int getPortId(int physicalAddress) {
269        return tv().getPortId(physicalAddress);
270    }
271
272    private void handleSetOsdName(HdmiCecMessage cmd) {
273        Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
274
275        DeviceInfo current = mDevices.get(mProcessedDeviceCount);
276        if (current.mLogicalAddress != cmd.getSource()) {
277            Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
278                    cmd.getSource());
279            return;
280        }
281
282        String displayName = null;
283        try {
284            displayName = new String(cmd.getParams(), "US-ASCII");
285        } catch (UnsupportedEncodingException e) {
286            Slog.w(TAG, "Failed to decode display name: " + cmd.toString());
287            // If failed to get display name, use the default name of device.
288            displayName = HdmiUtils.getDefaultDeviceName(current.mLogicalAddress);
289        }
290        current.mDisplayName = displayName;
291        increaseProcessedDeviceCount();
292        checkAndProceedStage();
293    }
294
295    private void handleVendorId(HdmiCecMessage cmd) {
296        Preconditions.checkState(mProcessedDeviceCount < mDevices.size());
297
298        DeviceInfo current = mDevices.get(mProcessedDeviceCount);
299        if (current.mLogicalAddress != cmd.getSource()) {
300            Slog.w(TAG, "Unmatched address[expected:" + current.mLogicalAddress + ", actual:" +
301                    cmd.getSource());
302            return;
303        }
304
305        byte[] params = cmd.getParams();
306        int vendorId = HdmiUtils.threeBytesToInt(params);
307        current.mVendorId = vendorId;
308        increaseProcessedDeviceCount();
309        checkAndProceedStage();
310    }
311
312    private void increaseProcessedDeviceCount() {
313        mProcessedDeviceCount++;
314        mTimeoutRetry = 0;
315    }
316
317    private void removeDevice(int index) {
318        mDevices.remove(index);
319    }
320
321    private void wrapUpAndFinish() {
322        Slog.v(TAG, "---------Wrap up Device Discovery:[" + mDevices.size() + "]---------");
323        ArrayList<HdmiDeviceInfo> result = new ArrayList<>();
324        for (DeviceInfo info : mDevices) {
325            HdmiDeviceInfo cecDeviceInfo = info.toHdmiDeviceInfo();
326            Slog.v(TAG, " DeviceInfo: " + cecDeviceInfo);
327            result.add(cecDeviceInfo);
328        }
329        Slog.v(TAG, "--------------------------------------------");
330        mCallback.onDeviceDiscoveryDone(result);
331        finish();
332        // Process any commands buffered while device discovery action was in progress.
333        tv().processAllDelayedMessages();
334    }
335
336    private void checkAndProceedStage() {
337        if (mDevices.isEmpty()) {
338            wrapUpAndFinish();
339            return;
340        }
341
342        // If finished current stage, move on to next stage.
343        if (mProcessedDeviceCount == mDevices.size()) {
344            mProcessedDeviceCount = 0;
345            switch (mState) {
346                case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
347                    startOsdNameStage();
348                    return;
349                case STATE_WAITING_FOR_OSD_NAME:
350                    startVendorIdStage();
351                    return;
352                case STATE_WAITING_FOR_VENDOR_ID:
353                    wrapUpAndFinish();
354                    return;
355                default:
356                    return;
357            }
358        } else {
359            sendQueryCommand();
360        }
361    }
362
363    private void sendQueryCommand() {
364        int address = mDevices.get(mProcessedDeviceCount).mLogicalAddress;
365        switch (mState) {
366            case STATE_WAITING_FOR_PHYSICAL_ADDRESS:
367                queryPhysicalAddress(address);
368                return;
369            case STATE_WAITING_FOR_OSD_NAME:
370                queryOsdName(address);
371                return;
372            case STATE_WAITING_FOR_VENDOR_ID:
373                queryVendorId(address);
374            default:
375                return;
376        }
377    }
378
379    @Override
380    void handleTimerEvent(int state) {
381        if (mState == STATE_NONE || mState != state) {
382            return;
383        }
384
385        if (++mTimeoutRetry < HdmiConfig.TIMEOUT_RETRY) {
386            sendQueryCommand();
387            return;
388        }
389        mTimeoutRetry = 0;
390        Slog.v(TAG, "Timeout[State=" + mState + ", Processed=" + mProcessedDeviceCount);
391        removeDevice(mProcessedDeviceCount);
392        checkAndProceedStage();
393    }
394}
395