HdmiCecLocalDeviceTv.java revision 4893c7efde52411ad051ef5c20251439f4098eac
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    /**
91     * Performs the action routing control.
92     *
93     * @param portId new HDMI port to route to
94     * @param callback callback object to report the result with
95     */
96    void portSelect(int portId, IHdmiControlCallback callback) {
97        assertRunOnServiceThread();
98        if (isInPresetInstallationMode()) {
99            invokeCallback(callback, HdmiCec.RESULT_INCORRECT_MODE);
100            return;
101        }
102        // Make sure this call does not stem from <Active Source> message reception, in
103        // which case the two ports will be the same.
104        if (portId == getActivePortId()) {
105            invokeCallback(callback, HdmiCec.RESULT_SUCCESS);
106            return;
107        }
108        setActivePortId(portId);
109
110        // TODO: Return immediately if the operation is triggered by <Text/Image View On>
111        // TODO: Handle invalid port id / active input which should be treated as an
112        //        internal tuner.
113
114        removeAction(RoutingControlAction.class);
115
116        int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId()));
117        int newPath = mService.portIdToPath(portId);
118        HdmiCecMessage routingChange =
119                HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
120        mService.sendCecCommand(routingChange);
121        addAndStartAction(new RoutingControlAction(this, newPath, callback));
122    }
123
124    /**
125     * Sends key to a target CEC device.
126     *
127     * @param keyCode key code to send. Defined in {@link KeyEvent}.
128     * @param isPressed true if this is keypress event
129     */
130    void sendKeyEvent(int keyCode, boolean isPressed) {
131        assertRunOnServiceThread();
132        List<SendKeyAction> action = getActions(SendKeyAction.class);
133        if (!action.isEmpty()) {
134            action.get(0).processKeyEvent(keyCode, isPressed);
135        } else {
136            if (isPressed) {
137                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
138            } else {
139                Slog.w(TAG, "Discard key release event");
140            }
141        }
142    }
143
144    private static void invokeCallback(IHdmiControlCallback callback, int result) {
145        if (callback == null) {
146            return;
147        }
148        try {
149            callback.onComplete(result);
150        } catch (RemoteException e) {
151            Slog.e(TAG, "Invoking callback failed:" + e);
152        }
153    }
154
155    @Override
156    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
157        assertRunOnServiceThread();
158        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
159                mAddress, Locale.getDefault().getISO3Language());
160        // TODO: figure out how to handle failed to get language code.
161        if (command != null) {
162            mService.sendCecCommand(command);
163        } else {
164            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
165        }
166        return true;
167    }
168
169    @Override
170    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
171        assertRunOnServiceThread();
172        // Ignore if [Device Discovery Action] is going on.
173        if (hasAction(DeviceDiscoveryAction.class)) {
174            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
175                    + "because Device Discovery Action is on-going:" + message);
176            return true;
177        }
178
179        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
180        int logicalAddress = message.getSource();
181
182        // If it is a new device and connected to the tail of active path,
183        // it's required to change routing path.
184        boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress)
185                && isTailOfActivePath(physicalAddress);
186        addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress,
187                requireRoutingChange));
188        return true;
189    }
190
191    @Override
192    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
193        assertRunOnServiceThread();
194        List<VendorSpecificAction> actions = Collections.emptyList();
195        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
196
197        // We assume that there can be multiple vendor-specific command actions running
198        // at the same time. Pass the message to each action to see if one of them needs it.
199        for (VendorSpecificAction action : actions) {
200            if (action.processCommand(message)) {
201                return true;
202            }
203        }
204        // Handle the message here if it is not already consumed by one of the running actions.
205        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
206        // vendor-specific action:
207        //
208        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
209        //
210        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
211        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
212                message.getDestination(), message.getSource(), message.getOpcode(),
213                HdmiConstants.ABORT_REFUSED));
214        return true;
215    }
216
217    private void launchDeviceDiscovery() {
218        assertRunOnServiceThread();
219        clearDeviceInfoList();
220        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
221                new DeviceDiscoveryCallback() {
222                    @Override
223                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
224                        for (HdmiCecDeviceInfo info : deviceInfos) {
225                            addCecDevice(info);
226                        }
227
228                        // Since we removed all devices when it's start and
229                        // device discovery action does not poll local devices,
230                        // we should put device info of local device manually here
231                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
232                            addCecDevice(device.getDeviceInfo());
233                        }
234
235                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
236
237                        // If there is AVR, initiate System Audio Auto initiation action,
238                        // which turns on and off system audio according to last system
239                        // audio setting.
240                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
241                        if (avrInfo != null) {
242                            addAndStartAction(new SystemAudioAutoInitiationAction(
243                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
244                        }
245                    }
246                });
247        addAndStartAction(action);
248    }
249
250    // Clear all device info.
251    private void clearDeviceInfoList() {
252        assertRunOnServiceThread();
253        mDeviceInfos.clear();
254    }
255
256    void setSystemAudioMode(boolean on) {
257        synchronized (mLock) {
258            if (on != mSystemAudioMode) {
259                mSystemAudioMode = on;
260                // TODO: Need to set the preference for SystemAudioMode.
261                // TODO: Need to handle the notification of changing the mode and
262                // to identify the notification should be handled in the service or TvSettings.
263            }
264        }
265    }
266
267    boolean getSystemAudioMode() {
268        synchronized (mLock) {
269            assertRunOnServiceThread();
270            return mSystemAudioMode;
271        }
272    }
273
274    /**
275     * Change ARC status into the given {@code enabled} status.
276     *
277     * @return {@code true} if ARC was in "Enabled" status
278     */
279    boolean setArcStatus(boolean enabled) {
280        synchronized (mLock) {
281            boolean oldStatus = mArcStatusEnabled;
282            // 1. Enable/disable ARC circuit.
283            mService.setAudioReturnChannel(enabled);
284
285            // TODO: notify arc mode change to AudioManager.
286
287            // 2. Update arc status;
288            mArcStatusEnabled = enabled;
289            return oldStatus;
290        }
291    }
292
293    /**
294     * Returns whether ARC is enabled or not.
295     */
296    boolean getArcStatus() {
297        synchronized (mLock) {
298            return mArcStatusEnabled;
299        }
300    }
301
302    void setAudioStatus(boolean mute, int volume) {
303        mService.setAudioStatus(mute, volume);
304    }
305
306    @Override
307    protected boolean handleInitiateArc(HdmiCecMessage message) {
308        assertRunOnServiceThread();
309        // In case where <Initiate Arc> is started by <Request ARC Initiation>
310        // need to clean up RequestArcInitiationAction.
311        removeAction(RequestArcInitiationAction.class);
312        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
313                message.getSource(), true);
314        addAndStartAction(action);
315        return true;
316    }
317
318    @Override
319    protected boolean handleTerminateArc(HdmiCecMessage message) {
320        assertRunOnServiceThread();
321        // In case where <Terminate Arc> is started by <Request ARC Termination>
322        // need to clean up RequestArcInitiationAction.
323        // TODO: check conditions of power status by calling is_connected api
324        // to be added soon.
325        removeAction(RequestArcTerminationAction.class);
326        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
327                message.getSource(), false);
328        addAndStartAction(action);
329        return true;
330    }
331
332    @Override
333    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
334        assertRunOnServiceThread();
335        if (!isMessageForSystemAudio(message)) {
336            return false;
337        }
338        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
339                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message));
340        addAndStartAction(action);
341        return true;
342    }
343
344    @Override
345    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
346        assertRunOnServiceThread();
347        if (!isMessageForSystemAudio(message)) {
348            return false;
349        }
350        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
351        return true;
352    }
353
354    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
355        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
356                || message.getDestination() != HdmiCec.ADDR_TV
357                || getAvrDeviceInfo() == null) {
358            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
359            return false;
360        }
361        return true;
362    }
363
364    /**
365     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
366     * logical address as new device info's.
367     *
368     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
369     *
370     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
371     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
372     *         that has the same logical address as new one has.
373     */
374    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
375        assertRunOnServiceThread();
376        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
377        if (oldDeviceInfo != null) {
378            removeDeviceInfo(deviceInfo.getLogicalAddress());
379        }
380        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
381        return oldDeviceInfo;
382    }
383
384    /**
385     * Remove a device info corresponding to the given {@code logicalAddress}.
386     * It returns removed {@link HdmiCecDeviceInfo} if exists.
387     *
388     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
389     *
390     * @param logicalAddress logical address of device to be removed
391     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
392     */
393    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
394        assertRunOnServiceThread();
395        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
396        if (deviceInfo != null) {
397            mDeviceInfos.remove(logicalAddress);
398        }
399        return deviceInfo;
400    }
401
402    /**
403     * Return a list of all {@link HdmiCecDeviceInfo}.
404     *
405     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
406     */
407    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
408        assertRunOnServiceThread();
409        if (includelLocalDevice) {
410                return HdmiUtils.sparseArrayToList(mDeviceInfos);
411        } else {
412
413            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
414            for (int i = 0; i < mDeviceInfos.size(); ++i) {
415                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
416                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
417                    infoList.add(info);
418                }
419            }
420            return infoList;
421        }
422    }
423
424    private boolean isLocalDeviceAddress(int address) {
425        assertRunOnServiceThread();
426        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
427            if (device.isAddressOf(address)) {
428                return true;
429            }
430        }
431        return false;
432    }
433
434    /**
435     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
436     *
437     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
438     *
439     * @param logicalAddress logical address to be retrieved
440     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
441     *         Returns null if no logical address matched
442     */
443    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
444        assertRunOnServiceThread();
445        return mDeviceInfos.get(logicalAddress);
446    }
447
448    HdmiCecDeviceInfo getAvrDeviceInfo() {
449        assertRunOnServiceThread();
450        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
451    }
452
453    /**
454     * Called when a device is newly added or a new device is detected.
455     *
456     * @param info device info of a new device.
457     */
458    final void addCecDevice(HdmiCecDeviceInfo info) {
459        assertRunOnServiceThread();
460        addDeviceInfo(info);
461        mService.invokeDeviceEventListeners(info, true);
462        // TODO: announce new device detection.
463    }
464
465    /**
466     * Called when a device is removed or removal of device is detected.
467     *
468     * @param address a logical address of a device to be removed
469     */
470    final void removeCecDevice(int address) {
471        assertRunOnServiceThread();
472        HdmiCecDeviceInfo info = removeDeviceInfo(address);
473        mCecMessageCache.flushMessagesFrom(address);
474        mService.invokeDeviceEventListeners(info, false);
475    }
476
477    /**
478     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
479     * the given routing path. CEC devices use routing path for its physical address to
480     * describe the hierarchy of the devices in the network.
481     *
482     * @param path routing path or physical address
483     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
484     */
485    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
486        assertRunOnServiceThread();
487        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
488            if (info.getPhysicalAddress() == path) {
489                return info;
490            }
491        }
492        return null;
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 physicalAddress physical address of a device to be searched
501     * @param logicalAddress logical address of a device to be searched
502     * @return true if exist; otherwise false
503     */
504    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
505        assertRunOnServiceThread();
506        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
507        if (device == null) {
508            return false;
509        }
510        return device.getPhysicalAddress() == physicalAddress;
511    }
512
513    @Override
514    void onHotplug(int portNo, boolean connected) {
515        assertRunOnServiceThread();
516        // TODO: delegate onHotplug event to each local device.
517
518        // Tv device will have permanent HotplugDetectionAction.
519        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
520        if (!hotplugActions.isEmpty()) {
521            // Note that hotplug action is single action running on a machine.
522            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
523            hotplugActions.get(0).pollAllDevicesNow();
524        }
525    }
526
527    boolean canChangeSystemAudio() {
528        // TODO: implement this.
529        // return true if no system audio control sequence is running.
530        return false;
531    }
532}
533