HdmiCecLocalDeviceTv.java revision 187d01765b935d07936f74343b4f4af590c239a1
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.HdmiCec;
20import android.hardware.hdmi.HdmiCecDeviceInfo;
21import android.hardware.hdmi.HdmiCecMessage;
22import android.hardware.hdmi.IHdmiControlCallback;
23import android.os.RemoteException;
24import android.util.Slog;
25import android.util.SparseArray;
26
27import com.android.internal.annotations.GuardedBy;
28import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
29
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.List;
33import java.util.Locale;
34
35/**
36 * Represent a logical device of type TV residing in Android system.
37 */
38final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
39    private static final String TAG = "HdmiCecLocalDeviceTv";
40
41    // Whether ARC is "enabled" or not.
42    @GuardedBy("mLock")
43    private boolean mArcStatusEnabled = false;
44
45    @GuardedBy("mLock")
46    // Whether SystemAudioMode is "On" or not.
47    private boolean mSystemAudioMode;
48
49    // Map-like container of all cec devices including local ones.
50    // A logical address of device is used as key of container.
51    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
52
53    HdmiCecLocalDeviceTv(HdmiControlService service) {
54        super(service, HdmiCec.DEVICE_TV);
55
56        // TODO: load system audio mode and set it to mSystemAudioMode.
57    }
58
59    @Override
60    protected void onAddressAllocated(int logicalAddress) {
61        assertRunOnServiceThread();
62        // TODO: vendor-specific initialization here.
63
64        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
65                mAddress, mService.getPhysicalAddress(), mDeviceType));
66        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
67                mAddress, mService.getVendorId()));
68
69        launchDeviceDiscovery();
70        // TODO: Start routing control action, device discovery action.
71    }
72
73    /**
74     * Performs the action 'device select', or 'one touch play' initiated by TV.
75     *
76     * @param targetAddress logical address of the device to select
77     * @param callback callback object to report the result with
78     */
79    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
80        assertRunOnServiceThread();
81        HdmiCecDeviceInfo targetDevice = mService.getDeviceInfo(targetAddress);
82        if (targetDevice == null) {
83            invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE);
84            return;
85        }
86        removeAction(DeviceSelectAction.class);
87        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
88    }
89
90    private static void invokeCallback(IHdmiControlCallback callback, int result) {
91        try {
92            callback.onComplete(result);
93        } catch (RemoteException e) {
94            Slog.e(TAG, "Invoking callback failed:" + e);
95        }
96    }
97
98    @Override
99    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
100        assertRunOnServiceThread();
101        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
102                mAddress, Locale.getDefault().getISO3Language());
103        // TODO: figure out how to handle failed to get language code.
104        if (command != null) {
105            mService.sendCecCommand(command);
106        } else {
107            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
108        }
109        return true;
110    }
111
112    @Override
113    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
114        assertRunOnServiceThread();
115        // Ignore if [Device Discovery Action] is going on.
116        if (hasAction(DeviceDiscoveryAction.class)) {
117            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
118                    + "because Device Discovery Action is on-going:" + message);
119            return true;
120        }
121
122        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
123        int logicalAddress = message.getSource();
124
125        // If it is a new device and connected to the tail of active path,
126        // it's required to change routing path.
127        boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress)
128                && isTailOfActivePath(physicalAddress);
129        addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress,
130                requireRoutingChange));
131        return true;
132    }
133
134    @Override
135    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
136        assertRunOnServiceThread();
137        List<VendorSpecificAction> actions = Collections.emptyList();
138        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
139
140        // We assume that there can be multiple vendor-specific command actions running
141        // at the same time. Pass the message to each action to see if one of them needs it.
142        for (VendorSpecificAction action : actions) {
143            if (action.processCommand(message)) {
144                return true;
145            }
146        }
147        // Handle the message here if it is not already consumed by one of the running actions.
148        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
149        // vendor-specific action:
150        //
151        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
152        //
153        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
154        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
155                message.getDestination(), message.getSource(), message.getOpcode(),
156                HdmiConstants.ABORT_REFUSED));
157        return true;
158    }
159
160    private void launchDeviceDiscovery() {
161        assertRunOnServiceThread();
162        clearDeviceInfoList();
163        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
164                new DeviceDiscoveryCallback() {
165                    @Override
166                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
167                        for (HdmiCecDeviceInfo info : deviceInfos) {
168                            addCecDevice(info);
169                        }
170
171                        // Since we removed all devices when it's start and
172                        // device discovery action does not poll local devices,
173                        // we should put device info of local device manually here
174                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
175                            addCecDevice(device.getDeviceInfo());
176                        }
177
178                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
179
180                        // If there is AVR, initiate System Audio Auto initiation action,
181                        // which turns on and off system audio according to last system
182                        // audio setting.
183                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
184                        if (avrInfo != null) {
185                            addAndStartAction(new SystemAudioAutoInitiationAction(
186                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
187                        }
188                    }
189                });
190        addAndStartAction(action);
191    }
192
193    // Clear all device info.
194    private void clearDeviceInfoList() {
195        assertRunOnServiceThread();
196        mDeviceInfos.clear();
197    }
198
199    void setSystemAudioMode(boolean on) {
200        synchronized (mLock) {
201            if (on != mSystemAudioMode) {
202                mSystemAudioMode = on;
203                // TODO: Need to set the preference for SystemAudioMode.
204                // TODO: Need to handle the notification of changing the mode and
205                // to identify the notification should be handled in the service or TvSettings.
206            }
207        }
208    }
209
210    boolean getSystemAudioMode() {
211        synchronized (mLock) {
212            assertRunOnServiceThread();
213            return mSystemAudioMode;
214        }
215    }
216
217    /**
218     * Change ARC status into the given {@code enabled} status.
219     *
220     * @return {@code true} if ARC was in "Enabled" status
221     */
222    boolean setArcStatus(boolean enabled) {
223        synchronized (mLock) {
224            boolean oldStatus = mArcStatusEnabled;
225            // 1. Enable/disable ARC circuit.
226            mService.setAudioReturnChannel(enabled);
227
228            // TODO: notify arc mode change to AudioManager.
229
230            // 2. Update arc status;
231            mArcStatusEnabled = enabled;
232            return oldStatus;
233        }
234    }
235
236    /**
237     * Returns whether ARC is enabled or not.
238     */
239    boolean getArcStatus() {
240        synchronized (mLock) {
241            return mArcStatusEnabled;
242        }
243    }
244
245    void setAudioStatus(boolean mute, int volume) {
246        mService.setAudioStatus(mute, volume);
247    }
248
249    @Override
250    protected boolean handleInitiateArc(HdmiCecMessage message) {
251        assertRunOnServiceThread();
252        // In case where <Initiate Arc> is started by <Request ARC Initiation>
253        // need to clean up RequestArcInitiationAction.
254        removeAction(RequestArcInitiationAction.class);
255        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
256                message.getSource(), true);
257        addAndStartAction(action);
258        return true;
259    }
260
261    @Override
262    protected boolean handleTerminateArc(HdmiCecMessage message) {
263        assertRunOnServiceThread();
264        // In case where <Terminate Arc> is started by <Request ARC Termination>
265        // need to clean up RequestArcInitiationAction.
266        // TODO: check conditions of power status by calling is_connected api
267        // to be added soon.
268        removeAction(RequestArcTerminationAction.class);
269        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
270                message.getSource(), false);
271        addAndStartAction(action);
272        return true;
273    }
274
275    @Override
276    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
277        assertRunOnServiceThread();
278        if (!isMessageForSystemAudio(message)) {
279            return false;
280        }
281        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
282                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message));
283        addAndStartAction(action);
284        return true;
285    }
286
287    @Override
288    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
289        assertRunOnServiceThread();
290        if (!isMessageForSystemAudio(message)) {
291            return false;
292        }
293        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
294        return true;
295    }
296
297    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
298        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
299                || message.getDestination() != HdmiCec.ADDR_TV
300                || getAvrDeviceInfo() == null) {
301            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
302            return false;
303        }
304        return true;
305    }
306
307    /**
308     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
309     * logical address as new device info's.
310     *
311     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
312     *
313     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
314     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
315     *         that has the same logical address as new one has.
316     */
317    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
318        assertRunOnServiceThread();
319        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
320        if (oldDeviceInfo != null) {
321            removeDeviceInfo(deviceInfo.getLogicalAddress());
322        }
323        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
324        return oldDeviceInfo;
325    }
326
327    /**
328     * Remove a device info corresponding to the given {@code logicalAddress}.
329     * It returns removed {@link HdmiCecDeviceInfo} if exists.
330     *
331     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
332     *
333     * @param logicalAddress logical address of device to be removed
334     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
335     */
336    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
337        assertRunOnServiceThread();
338        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
339        if (deviceInfo != null) {
340            mDeviceInfos.remove(logicalAddress);
341        }
342        return deviceInfo;
343    }
344
345    /**
346     * Return a list of all {@link HdmiCecDeviceInfo}.
347     *
348     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
349     */
350    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
351        assertRunOnServiceThread();
352        if (includelLocalDevice) {
353                return HdmiUtils.sparseArrayToList(mDeviceInfos);
354        } else {
355
356            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
357            for (int i = 0; i < mDeviceInfos.size(); ++i) {
358                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
359                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
360                    infoList.add(info);
361                }
362            }
363            return infoList;
364        }
365    }
366
367    private boolean isLocalDeviceAddress(int address) {
368        assertRunOnServiceThread();
369        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
370            if (device.isAddressOf(address)) {
371                return true;
372            }
373        }
374        return false;
375    }
376
377    /**
378     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
379     *
380     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
381     *
382     * @param logicalAddress logical address to be retrieved
383     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
384     *         Returns null if no logical address matched
385     */
386    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
387        assertRunOnServiceThread();
388        return mDeviceInfos.get(logicalAddress);
389    }
390
391    HdmiCecDeviceInfo getAvrDeviceInfo() {
392        assertRunOnServiceThread();
393        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
394    }
395
396    /**
397     * Called when a device is newly added or a new device is detected.
398     *
399     * @param info device info of a new device.
400     */
401    final void addCecDevice(HdmiCecDeviceInfo info) {
402        assertRunOnServiceThread();
403        addDeviceInfo(info);
404
405        // TODO: announce new device detection.
406    }
407
408    /**
409     * Called when a device is removed or removal of device is detected.
410     *
411     * @param address a logical address of a device to be removed
412     */
413    final void removeCecDevice(int address) {
414        assertRunOnServiceThread();
415        removeDeviceInfo(address);
416        mCecMessageCache.flushMessagesFrom(address);
417
418        // TODO: announce a device removal.
419    }
420
421    /**
422     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
423     * the given routing path. CEC devices use routing path for its physical address to
424     * describe the hierarchy of the devices in the network.
425     *
426     * @param path routing path or physical address
427     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
428     */
429    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
430        assertRunOnServiceThread();
431        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
432            if (info.getPhysicalAddress() == path) {
433                return info;
434            }
435        }
436        return null;
437    }
438
439    /**
440     * Whether a device of the specified physical address and logical address exists
441     * in a device info list. However, both are minimal condition and it could
442     * be different device from the original one.
443     *
444     * @param physicalAddress physical address of a device to be searched
445     * @param logicalAddress logical address of a device to be searched
446     * @return true if exist; otherwise false
447     */
448    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
449        assertRunOnServiceThread();
450        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
451        if (device == null) {
452            return false;
453        }
454        return device.getPhysicalAddress() == physicalAddress;
455    }
456
457    @Override
458    void onHotplug(int portNo, boolean connected) {
459        assertRunOnServiceThread();
460        // TODO: delegate onHotplug event to each local device.
461
462        // Tv device will have permanent HotplugDetectionAction.
463        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
464        if (!hotplugActions.isEmpty()) {
465            // Note that hotplug action is single action running on a machine.
466            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
467            hotplugActions.get(0).pollAllDevicesNow();
468        }
469    }
470
471    boolean canChangeSystemAudio() {
472        // TODO: implement this.
473        // return true if no system audio control sequence is running.
474        return false;
475    }
476}
477