HdmiCecLocalDeviceTv.java revision ea67c183fe5511ad99aeaae1a32b5245bd020e36
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 = 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 changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
257        assertRunOnServiceThread();
258        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
259        if (avr == null) {
260            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
261            return;
262        }
263
264        addAndStartAction(
265                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
266   }
267
268    boolean canChangeSystemAudioMode() {
269        // TODO: once have immutable device info, test whether avr info exists or not.
270        return false;
271    }
272
273    void setSystemAudioMode(boolean on) {
274        synchronized (mLock) {
275            if (on != mSystemAudioMode) {
276                mSystemAudioMode = on;
277                // TODO: Need to set the preference for SystemAudioMode.
278                mService.announceSystemAudioModeChange(on);
279            }
280        }
281    }
282
283    boolean getSystemAudioMode() {
284        synchronized (mLock) {
285            return mSystemAudioMode;
286        }
287    }
288
289    /**
290     * Change ARC status into the given {@code enabled} status.
291     *
292     * @return {@code true} if ARC was in "Enabled" status
293     */
294    boolean setArcStatus(boolean enabled) {
295        synchronized (mLock) {
296            boolean oldStatus = mArcStatusEnabled;
297            // 1. Enable/disable ARC circuit.
298            mService.setAudioReturnChannel(enabled);
299
300            // TODO: notify arc mode change to AudioManager.
301
302            // 2. Update arc status;
303            mArcStatusEnabled = enabled;
304            return oldStatus;
305        }
306    }
307
308    /**
309     * Returns whether ARC is enabled or not.
310     */
311    boolean getArcStatus() {
312        synchronized (mLock) {
313            return mArcStatusEnabled;
314        }
315    }
316
317    void setAudioStatus(boolean mute, int volume) {
318        mService.setAudioStatus(mute, volume);
319    }
320
321    @Override
322    protected boolean handleInitiateArc(HdmiCecMessage message) {
323        assertRunOnServiceThread();
324        // In case where <Initiate Arc> is started by <Request ARC Initiation>
325        // need to clean up RequestArcInitiationAction.
326        removeAction(RequestArcInitiationAction.class);
327        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
328                message.getSource(), true);
329        addAndStartAction(action);
330        return true;
331    }
332
333    @Override
334    protected boolean handleTerminateArc(HdmiCecMessage message) {
335        assertRunOnServiceThread();
336        // In case where <Terminate Arc> is started by <Request ARC Termination>
337        // need to clean up RequestArcInitiationAction.
338        // TODO: check conditions of power status by calling is_connected api
339        // to be added soon.
340        removeAction(RequestArcTerminationAction.class);
341        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
342                message.getSource(), false);
343        addAndStartAction(action);
344        return true;
345    }
346
347    @Override
348    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
349        assertRunOnServiceThread();
350        if (!isMessageForSystemAudio(message)) {
351            return false;
352        }
353        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
354                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message));
355        addAndStartAction(action);
356        return true;
357    }
358
359    @Override
360    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
361        assertRunOnServiceThread();
362        if (!isMessageForSystemAudio(message)) {
363            return false;
364        }
365        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
366        return true;
367    }
368
369    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
370        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
371                || message.getDestination() != HdmiCec.ADDR_TV
372                || getAvrDeviceInfo() == null) {
373            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
374            return false;
375        }
376        return true;
377    }
378
379    /**
380     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
381     * logical address as new device info's.
382     *
383     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
384     *
385     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
386     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
387     *         that has the same logical address as new one has.
388     */
389    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
390        assertRunOnServiceThread();
391        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
392        if (oldDeviceInfo != null) {
393            removeDeviceInfo(deviceInfo.getLogicalAddress());
394        }
395        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
396        return oldDeviceInfo;
397    }
398
399    /**
400     * Remove a device info corresponding to the given {@code logicalAddress}.
401     * It returns removed {@link HdmiCecDeviceInfo} if exists.
402     *
403     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
404     *
405     * @param logicalAddress logical address of device to be removed
406     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
407     */
408    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
409        assertRunOnServiceThread();
410        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
411        if (deviceInfo != null) {
412            mDeviceInfos.remove(logicalAddress);
413        }
414        return deviceInfo;
415    }
416
417    /**
418     * Return a list of all {@link HdmiCecDeviceInfo}.
419     *
420     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
421     */
422    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
423        assertRunOnServiceThread();
424        if (includelLocalDevice) {
425                return HdmiUtils.sparseArrayToList(mDeviceInfos);
426        } else {
427
428            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
429            for (int i = 0; i < mDeviceInfos.size(); ++i) {
430                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
431                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
432                    infoList.add(info);
433                }
434            }
435            return infoList;
436        }
437    }
438
439    private boolean isLocalDeviceAddress(int address) {
440        assertRunOnServiceThread();
441        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
442            if (device.isAddressOf(address)) {
443                return true;
444            }
445        }
446        return false;
447    }
448
449    /**
450     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
451     *
452     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
453     *
454     * @param logicalAddress logical address to be retrieved
455     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
456     *         Returns null if no logical address matched
457     */
458    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
459        assertRunOnServiceThread();
460        return mDeviceInfos.get(logicalAddress);
461    }
462
463    HdmiCecDeviceInfo getAvrDeviceInfo() {
464        assertRunOnServiceThread();
465        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
466    }
467
468    /**
469     * Called when a device is newly added or a new device is detected.
470     *
471     * @param info device info of a new device.
472     */
473    final void addCecDevice(HdmiCecDeviceInfo info) {
474        assertRunOnServiceThread();
475        addDeviceInfo(info);
476        if (info.getLogicalAddress() == mAddress) {
477            // The addition of TV device itself should not be notified.
478            return;
479        }
480        mService.invokeDeviceEventListeners(info, true);
481    }
482
483    /**
484     * Called when a device is removed or removal of device is detected.
485     *
486     * @param address a logical address of a device to be removed
487     */
488    final void removeCecDevice(int address) {
489        assertRunOnServiceThread();
490        HdmiCecDeviceInfo info = removeDeviceInfo(address);
491        mCecMessageCache.flushMessagesFrom(address);
492        mService.invokeDeviceEventListeners(info, false);
493    }
494
495    /**
496     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
497     * the given routing path. CEC devices use routing path for its physical address to
498     * describe the hierarchy of the devices in the network.
499     *
500     * @param path routing path or physical address
501     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
502     */
503    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
504        assertRunOnServiceThread();
505        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
506            if (info.getPhysicalAddress() == path) {
507                return info;
508            }
509        }
510        return null;
511    }
512
513    /**
514     * Whether a device of the specified physical address and logical address exists
515     * in a device info list. However, both are minimal condition and it could
516     * be different device from the original one.
517     *
518     * @param physicalAddress physical address of a device to be searched
519     * @param logicalAddress logical address of a device to be searched
520     * @return true if exist; otherwise false
521     */
522    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
523        assertRunOnServiceThread();
524        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
525        if (device == null) {
526            return false;
527        }
528        return device.getPhysicalAddress() == physicalAddress;
529    }
530
531    @Override
532    void onHotplug(int portNo, boolean connected) {
533        assertRunOnServiceThread();
534        // TODO: delegate onHotplug event to each local device.
535
536        // Tv device will have permanent HotplugDetectionAction.
537        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
538        if (!hotplugActions.isEmpty()) {
539            // Note that hotplug action is single action running on a machine.
540            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
541            hotplugActions.get(0).pollAllDevicesNow();
542        }
543    }
544
545    boolean canChangeSystemAudio() {
546        // TODO: implement this.
547        // return true if no system audio control sequence is running.
548        return false;
549    }
550}
551