HdmiCecLocalDeviceTv.java revision e9cf1583c74fd03977c1ecb14520663710f14439
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;
29import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
30
31import java.util.ArrayList;
32import java.util.Collections;
33import java.util.List;
34import java.util.Locale;
35
36/**
37 * Represent a logical device of type TV residing in Android system.
38 */
39final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
40    private static final String TAG = "HdmiCecLocalDeviceTv";
41
42    // Whether ARC is "enabled" or not.
43    @GuardedBy("mLock")
44    private boolean mArcStatusEnabled = false;
45
46    // Whether SystemAudioMode is "On" or not.
47    @GuardedBy("mLock")
48    private boolean mSystemAudioMode;
49
50
51    // Copy of mDeviceInfos to guarantee thread-safety.
52    @GuardedBy("mLock")
53    private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
54    // All external cec device which excludes local devices.
55    @GuardedBy("mLock")
56    private List<HdmiCecDeviceInfo> mSafeExternalDeviceInfos = Collections.emptyList();
57
58    // Map-like container of all cec devices including local ones.
59    // A logical address of device is used as key of container.
60    // This is not thread-safe. For external purpose use mSafeDeviceInfos.
61    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
62
63    HdmiCecLocalDeviceTv(HdmiControlService service) {
64        super(service, HdmiCec.DEVICE_TV);
65
66        // TODO: load system audio mode and set it to mSystemAudioMode.
67    }
68
69    @Override
70    @ServiceThreadOnly
71    protected void onAddressAllocated(int logicalAddress) {
72        assertRunOnServiceThread();
73        // TODO: vendor-specific initialization here.
74
75        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
76                mAddress, mService.getPhysicalAddress(), mDeviceType));
77        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
78                mAddress, mService.getVendorId()));
79
80        launchDeviceDiscovery();
81        // TODO: Start routing control action
82    }
83
84    /**
85     * Performs the action 'device select', or 'one touch play' initiated by TV.
86     *
87     * @param targetAddress logical address of the device to select
88     * @param callback callback object to report the result with
89     */
90    @ServiceThreadOnly
91    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
92        assertRunOnServiceThread();
93        HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress);
94        if (targetDevice == null) {
95            invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE);
96            return;
97        }
98        removeAction(DeviceSelectAction.class);
99        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
100    }
101
102    /**
103     * Performs the action routing control.
104     *
105     * @param portId new HDMI port to route to
106     * @param callback callback object to report the result with
107     */
108    @ServiceThreadOnly
109    void portSelect(int portId, IHdmiControlCallback callback) {
110        assertRunOnServiceThread();
111        if (isInPresetInstallationMode()) {
112            invokeCallback(callback, HdmiCec.RESULT_INCORRECT_MODE);
113            return;
114        }
115        // Make sure this call does not stem from <Active Source> message reception, in
116        // which case the two ports will be the same.
117        if (portId == getActivePortId()) {
118            invokeCallback(callback, HdmiCec.RESULT_SUCCESS);
119            return;
120        }
121        setActivePortId(portId);
122
123        // TODO: Return immediately if the operation is triggered by <Text/Image View On>
124        // TODO: Handle invalid port id / active input which should be treated as an
125        //        internal tuner.
126
127        removeAction(RoutingControlAction.class);
128
129        int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId()));
130        int newPath = mService.portIdToPath(portId);
131        HdmiCecMessage routingChange =
132                HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
133        mService.sendCecCommand(routingChange);
134        addAndStartAction(new RoutingControlAction(this, newPath, callback));
135    }
136
137    /**
138     * Sends key to a target CEC device.
139     *
140     * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}.
141     * @param isPressed true if this is keypress event
142     */
143    @ServiceThreadOnly
144    void sendKeyEvent(int keyCode, boolean isPressed) {
145        assertRunOnServiceThread();
146        List<SendKeyAction> action = getActions(SendKeyAction.class);
147        if (!action.isEmpty()) {
148            action.get(0).processKeyEvent(keyCode, isPressed);
149        } else {
150            if (isPressed) {
151                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
152            } else {
153                Slog.w(TAG, "Discard key release event");
154            }
155        }
156    }
157
158    private static void invokeCallback(IHdmiControlCallback callback, int result) {
159        if (callback == null) {
160            return;
161        }
162        try {
163            callback.onComplete(result);
164        } catch (RemoteException e) {
165            Slog.e(TAG, "Invoking callback failed:" + e);
166        }
167    }
168
169    @Override
170    @ServiceThreadOnly
171    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
172        assertRunOnServiceThread();
173        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
174                mAddress, Locale.getDefault().getISO3Language());
175        // TODO: figure out how to handle failed to get language code.
176        if (command != null) {
177            mService.sendCecCommand(command);
178        } else {
179            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
180        }
181        return true;
182    }
183
184    @Override
185    @ServiceThreadOnly
186    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
187        assertRunOnServiceThread();
188        // Ignore if [Device Discovery Action] is going on.
189        if (hasAction(DeviceDiscoveryAction.class)) {
190            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
191                    + "because Device Discovery Action is on-going:" + message);
192            return true;
193        }
194
195        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
196        int logicalAddress = message.getSource();
197
198        // If it is a new device and connected to the tail of active path,
199        // it's required to change routing path.
200        boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress)
201                && isTailOfActivePath(physicalAddress);
202        addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress,
203                requireRoutingChange));
204        return true;
205    }
206
207    @Override
208    @ServiceThreadOnly
209    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
210        assertRunOnServiceThread();
211        List<VendorSpecificAction> actions = Collections.emptyList();
212        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
213
214        // We assume that there can be multiple vendor-specific command actions running
215        // at the same time. Pass the message to each action to see if one of them needs it.
216        for (VendorSpecificAction action : actions) {
217            if (action.processCommand(message)) {
218                return true;
219            }
220        }
221        // Handle the message here if it is not already consumed by one of the running actions.
222        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
223        // vendor-specific action:
224        //
225        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
226        //
227        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
228        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
229                message.getDestination(), message.getSource(), message.getOpcode(),
230                HdmiConstants.ABORT_REFUSED));
231        return true;
232    }
233
234    @ServiceThreadOnly
235    private void launchDeviceDiscovery() {
236        assertRunOnServiceThread();
237        clearDeviceInfoList();
238        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
239                new DeviceDiscoveryCallback() {
240                    @Override
241                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
242                        for (HdmiCecDeviceInfo info : deviceInfos) {
243                            addCecDevice(info);
244                        }
245
246                        // Since we removed all devices when it's start and
247                        // device discovery action does not poll local devices,
248                        // we should put device info of local device manually here
249                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
250                            addCecDevice(device.getDeviceInfo());
251                        }
252
253                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
254
255                        // If there is AVR, initiate System Audio Auto initiation action,
256                        // which turns on and off system audio according to last system
257                        // audio setting.
258                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
259                        if (avrInfo != null) {
260                            addAndStartAction(new SystemAudioAutoInitiationAction(
261                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
262                        }
263                    }
264                });
265        addAndStartAction(action);
266    }
267
268    // Clear all device info.
269    @ServiceThreadOnly
270    private void clearDeviceInfoList() {
271        assertRunOnServiceThread();
272        mDeviceInfos.clear();
273        updateSafeDeviceInfoList();
274    }
275
276    @ServiceThreadOnly
277    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
278        assertRunOnServiceThread();
279        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
280        if (avr == null) {
281            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
282            return;
283        }
284
285        addAndStartAction(
286                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
287    }
288
289    void setSystemAudioMode(boolean on) {
290        synchronized (mLock) {
291            if (on != mSystemAudioMode) {
292                mSystemAudioMode = on;
293                // TODO: Need to set the preference for SystemAudioMode.
294                mService.announceSystemAudioModeChange(on);
295            }
296        }
297    }
298
299    boolean getSystemAudioMode() {
300        synchronized (mLock) {
301            return mSystemAudioMode;
302        }
303    }
304
305    /**
306     * Change ARC status into the given {@code enabled} status.
307     *
308     * @return {@code true} if ARC was in "Enabled" status
309     */
310    boolean setArcStatus(boolean enabled) {
311        synchronized (mLock) {
312            boolean oldStatus = mArcStatusEnabled;
313            // 1. Enable/disable ARC circuit.
314            mService.setAudioReturnChannel(enabled);
315
316            // TODO: notify arc mode change to AudioManager.
317
318            // 2. Update arc status;
319            mArcStatusEnabled = enabled;
320            return oldStatus;
321        }
322    }
323
324    /**
325     * Returns whether ARC is enabled or not.
326     */
327    boolean getArcStatus() {
328        synchronized (mLock) {
329            return mArcStatusEnabled;
330        }
331    }
332
333    @ServiceThreadOnly
334    void setAudioStatus(boolean mute, int volume) {
335        mService.setAudioStatus(mute, volume);
336    }
337
338    @Override
339    @ServiceThreadOnly
340    protected boolean handleInitiateArc(HdmiCecMessage message) {
341        assertRunOnServiceThread();
342        // In case where <Initiate Arc> is started by <Request ARC Initiation>
343        // need to clean up RequestArcInitiationAction.
344        removeAction(RequestArcInitiationAction.class);
345        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
346                message.getSource(), true);
347        addAndStartAction(action);
348        return true;
349    }
350
351    @Override
352    @ServiceThreadOnly
353    protected boolean handleTerminateArc(HdmiCecMessage message) {
354        assertRunOnServiceThread();
355        // In case where <Terminate Arc> is started by <Request ARC Termination>
356        // need to clean up RequestArcInitiationAction.
357        // TODO: check conditions of power status by calling is_connected api
358        // to be added soon.
359        removeAction(RequestArcTerminationAction.class);
360        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
361                message.getSource(), false);
362        addAndStartAction(action);
363        return true;
364    }
365
366    @Override
367    @ServiceThreadOnly
368    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
369        assertRunOnServiceThread();
370        if (!isMessageForSystemAudio(message)) {
371            return false;
372        }
373        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
374                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
375        addAndStartAction(action);
376        return true;
377    }
378
379    @Override
380    @ServiceThreadOnly
381    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
382        assertRunOnServiceThread();
383        if (!isMessageForSystemAudio(message)) {
384            return false;
385        }
386        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
387        return true;
388    }
389
390    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
391        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
392                || message.getDestination() != HdmiCec.ADDR_TV
393                || getAvrDeviceInfo() == null) {
394            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
395            return false;
396        }
397        return true;
398    }
399
400    /**
401     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
402     * logical address as new device info's.
403     *
404     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
405     *
406     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
407     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
408     *         that has the same logical address as new one has.
409     */
410    @ServiceThreadOnly
411    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
412        assertRunOnServiceThread();
413        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
414        if (oldDeviceInfo != null) {
415            removeDeviceInfo(deviceInfo.getLogicalAddress());
416        }
417        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
418        updateSafeDeviceInfoList();
419        return oldDeviceInfo;
420    }
421
422    /**
423     * Remove a device info corresponding to the given {@code logicalAddress}.
424     * It returns removed {@link HdmiCecDeviceInfo} if exists.
425     *
426     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
427     *
428     * @param logicalAddress logical address of device to be removed
429     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
430     */
431    @ServiceThreadOnly
432    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
433        assertRunOnServiceThread();
434        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
435        if (deviceInfo != null) {
436            mDeviceInfos.remove(logicalAddress);
437        }
438        updateSafeDeviceInfoList();
439        return deviceInfo;
440    }
441
442    /**
443     * Return a list of all {@link HdmiCecDeviceInfo}.
444     *
445     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
446     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
447     */
448    @ServiceThreadOnly
449    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
450        assertRunOnServiceThread();
451        if (includelLocalDevice) {
452            return HdmiUtils.sparseArrayToList(mDeviceInfos);
453        } else {
454            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
455            for (int i = 0; i < mDeviceInfos.size(); ++i) {
456                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
457                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
458                    infoList.add(info);
459                }
460            }
461            return infoList;
462        }
463    }
464
465    /**
466     * Return a list of  {@link HdmiCecDeviceInfo}.
467     *
468     * @param includeLocalDevice whether to include local device in result.
469     */
470    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
471        synchronized (mLock) {
472            if (includeLocalDevice) {
473                return mSafeAllDeviceInfos;
474            } else {
475                return mSafeExternalDeviceInfos;
476            }
477        }
478    }
479
480    @ServiceThreadOnly
481    private void updateSafeDeviceInfoList() {
482        assertRunOnServiceThread();
483        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
484        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
485        synchronized (mLock) {
486            mSafeAllDeviceInfos = copiedDevices;
487            mSafeExternalDeviceInfos = externalDeviceInfos;
488        }
489    }
490
491    @ServiceThreadOnly
492    private boolean isLocalDeviceAddress(int address) {
493        assertRunOnServiceThread();
494        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
495            if (device.isAddressOf(address)) {
496                return true;
497            }
498        }
499        return false;
500    }
501
502    @ServiceThreadOnly
503    HdmiCecDeviceInfo getAvrDeviceInfo() {
504        assertRunOnServiceThread();
505        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
506    }
507
508    /**
509     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
510     *
511     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
512     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
513     *
514     * @param logicalAddress logical address to be retrieved
515     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
516     *         Returns null if no logical address matched
517     */
518    @ServiceThreadOnly
519    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
520        assertRunOnServiceThread();
521        return mDeviceInfos.get(logicalAddress);
522    }
523
524    boolean hasSystemAudioDevice() {
525        return getSafeAvrDeviceInfo() != null;
526    }
527
528    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
529        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
530    }
531
532    /**
533     * Thread safe version of {@link #getDeviceInfo(int)}.
534     *
535     * @param logicalAddress logical address to be retrieved
536     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
537     *         Returns null if no logical address matched
538     */
539    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
540        synchronized (mLock) {
541            return mSafeAllDeviceInfos.get(logicalAddress);
542        }
543    }
544
545    /**
546     * Called when a device is newly added or a new device is detected.
547     *
548     * @param info device info of a new device.
549     */
550    @ServiceThreadOnly
551    final void addCecDevice(HdmiCecDeviceInfo info) {
552        assertRunOnServiceThread();
553        addDeviceInfo(info);
554        if (info.getLogicalAddress() == mAddress) {
555            // The addition of TV device itself should not be notified.
556            return;
557        }
558        mService.invokeDeviceEventListeners(info, true);
559    }
560
561    /**
562     * Called when a device is removed or removal of device is detected.
563     *
564     * @param address a logical address of a device to be removed
565     */
566    @ServiceThreadOnly
567    final void removeCecDevice(int address) {
568        assertRunOnServiceThread();
569        HdmiCecDeviceInfo info = removeDeviceInfo(address);
570        mCecMessageCache.flushMessagesFrom(address);
571        mService.invokeDeviceEventListeners(info, false);
572    }
573
574    /**
575     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
576     * the given routing path. CEC devices use routing path for its physical address to
577     * describe the hierarchy of the devices in the network.
578     *
579     * @param path routing path or physical address
580     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
581     */
582    @ServiceThreadOnly
583    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
584        assertRunOnServiceThread();
585        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
586            if (info.getPhysicalAddress() == path) {
587                return info;
588            }
589        }
590        return null;
591    }
592
593    /**
594     * Whether a device of the specified physical address and logical address exists
595     * in a device info list. However, both are minimal condition and it could
596     * be different device from the original one.
597     *
598     * @param physicalAddress physical address of a device to be searched
599     * @param logicalAddress logical address of a device to be searched
600     * @return true if exist; otherwise false
601     */
602    @ServiceThreadOnly
603    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
604        assertRunOnServiceThread();
605        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
606        if (device == null) {
607            return false;
608        }
609        return device.getPhysicalAddress() == physicalAddress;
610    }
611
612    @Override
613    @ServiceThreadOnly
614    void onHotplug(int portNo, boolean connected) {
615        assertRunOnServiceThread();
616        // TODO: delegate onHotplug event to each local device.
617
618        // Tv device will have permanent HotplugDetectionAction.
619        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
620        if (!hotplugActions.isEmpty()) {
621            // Note that hotplug action is single action running on a machine.
622            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
623            hotplugActions.get(0).pollAllDevicesNow();
624        }
625    }
626
627    boolean canChangeSystemAudio() {
628        // TODO: implement this.
629        // return true if no system audio control sequence is running.
630        return false;
631    }
632}
633