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