HdmiCecLocalDeviceTv.java revision a858d221ff86c497e745222ea15bab141e337636
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.media.AudioSystem;
24import android.os.RemoteException;
25import android.util.Slog;
26import android.util.SparseArray;
27
28import com.android.internal.annotations.GuardedBy;
29import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
30import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
31
32import java.util.ArrayList;
33import java.util.Collections;
34import java.util.List;
35import java.util.Locale;
36
37/**
38 * Represent a logical device of type TV residing in Android system.
39 */
40final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
41    private static final String TAG = "HdmiCecLocalDeviceTv";
42
43    // Whether ARC is "enabled" or not.
44    @GuardedBy("mLock")
45    private boolean mArcStatusEnabled = false;
46
47    // Whether SystemAudioMode is "On" or not.
48    @GuardedBy("mLock")
49    private boolean mSystemAudioMode;
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            // 2. Notify arc status to audio service.
316            notifyArcStatusToAudioService(enabled);
317            // 3. Update arc status;
318            mArcStatusEnabled = enabled;
319            return oldStatus;
320        }
321    }
322
323    private void notifyArcStatusToAudioService(boolean enabled) {
324        // Note that we don't set any name to ARC.
325        mService.getAudioManager().setWiredDeviceConnectionState(
326                AudioSystem.DEVICE_OUT_HDMI_ARC,
327                enabled ? 1 : 0, "");
328    }
329
330    /**
331     * Returns whether ARC is enabled or not.
332     */
333    boolean getArcStatus() {
334        synchronized (mLock) {
335            return mArcStatusEnabled;
336        }
337    }
338
339    @ServiceThreadOnly
340    void setAudioStatus(boolean mute, int volume) {
341        mService.setAudioStatus(mute, volume);
342    }
343
344    @Override
345    @ServiceThreadOnly
346    protected boolean handleInitiateArc(HdmiCecMessage message) {
347        assertRunOnServiceThread();
348        // In case where <Initiate Arc> is started by <Request ARC Initiation>
349        // need to clean up RequestArcInitiationAction.
350        removeAction(RequestArcInitiationAction.class);
351        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
352                message.getSource(), true);
353        addAndStartAction(action);
354        return true;
355    }
356
357    @Override
358    @ServiceThreadOnly
359    protected boolean handleTerminateArc(HdmiCecMessage message) {
360        assertRunOnServiceThread();
361        // In case where <Terminate Arc> is started by <Request ARC Termination>
362        // need to clean up RequestArcInitiationAction.
363        // TODO: check conditions of power status by calling is_connected api
364        // to be added soon.
365        removeAction(RequestArcTerminationAction.class);
366        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
367                message.getSource(), false);
368        addAndStartAction(action);
369        return true;
370    }
371
372    @Override
373    @ServiceThreadOnly
374    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
375        assertRunOnServiceThread();
376        if (!isMessageForSystemAudio(message)) {
377            return false;
378        }
379        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
380                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
381        addAndStartAction(action);
382        return true;
383    }
384
385    @Override
386    @ServiceThreadOnly
387    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
388        assertRunOnServiceThread();
389        if (!isMessageForSystemAudio(message)) {
390            return false;
391        }
392        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
393        return true;
394    }
395
396    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
397        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
398                || message.getDestination() != HdmiCec.ADDR_TV
399                || getAvrDeviceInfo() == null) {
400            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
401            return false;
402        }
403        return true;
404    }
405
406    /**
407     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
408     * logical address as new device info's.
409     *
410     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
411     *
412     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
413     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
414     *         that has the same logical address as new one has.
415     */
416    @ServiceThreadOnly
417    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
418        assertRunOnServiceThread();
419        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
420        if (oldDeviceInfo != null) {
421            removeDeviceInfo(deviceInfo.getLogicalAddress());
422        }
423        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
424        updateSafeDeviceInfoList();
425        return oldDeviceInfo;
426    }
427
428    /**
429     * Remove a device info corresponding to the given {@code logicalAddress}.
430     * It returns removed {@link HdmiCecDeviceInfo} if exists.
431     *
432     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
433     *
434     * @param logicalAddress logical address of device to be removed
435     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
436     */
437    @ServiceThreadOnly
438    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
439        assertRunOnServiceThread();
440        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
441        if (deviceInfo != null) {
442            mDeviceInfos.remove(logicalAddress);
443        }
444        updateSafeDeviceInfoList();
445        return deviceInfo;
446    }
447
448    /**
449     * Return a list of all {@link HdmiCecDeviceInfo}.
450     *
451     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
452     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
453     */
454    @ServiceThreadOnly
455    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
456        assertRunOnServiceThread();
457        if (includelLocalDevice) {
458            return HdmiUtils.sparseArrayToList(mDeviceInfos);
459        } else {
460            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
461            for (int i = 0; i < mDeviceInfos.size(); ++i) {
462                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
463                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
464                    infoList.add(info);
465                }
466            }
467            return infoList;
468        }
469    }
470
471    /**
472     * Return a list of  {@link HdmiCecDeviceInfo}.
473     *
474     * @param includeLocalDevice whether to include local device in result.
475     */
476    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
477        synchronized (mLock) {
478            if (includeLocalDevice) {
479                return mSafeAllDeviceInfos;
480            } else {
481                return mSafeExternalDeviceInfos;
482            }
483        }
484    }
485
486    @ServiceThreadOnly
487    private void updateSafeDeviceInfoList() {
488        assertRunOnServiceThread();
489        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
490        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
491        synchronized (mLock) {
492            mSafeAllDeviceInfos = copiedDevices;
493            mSafeExternalDeviceInfos = externalDeviceInfos;
494        }
495    }
496
497    @ServiceThreadOnly
498    private boolean isLocalDeviceAddress(int address) {
499        assertRunOnServiceThread();
500        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
501            if (device.isAddressOf(address)) {
502                return true;
503            }
504        }
505        return false;
506    }
507
508    @ServiceThreadOnly
509    HdmiCecDeviceInfo getAvrDeviceInfo() {
510        assertRunOnServiceThread();
511        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
512    }
513
514    /**
515     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
516     *
517     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
518     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
519     *
520     * @param logicalAddress logical address to be retrieved
521     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
522     *         Returns null if no logical address matched
523     */
524    @ServiceThreadOnly
525    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
526        assertRunOnServiceThread();
527        return mDeviceInfos.get(logicalAddress);
528    }
529
530    boolean hasSystemAudioDevice() {
531        return getSafeAvrDeviceInfo() != null;
532    }
533
534    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
535        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
536    }
537
538    /**
539     * Thread safe version of {@link #getDeviceInfo(int)}.
540     *
541     * @param logicalAddress logical address to be retrieved
542     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
543     *         Returns null if no logical address matched
544     */
545    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
546        synchronized (mLock) {
547            return mSafeAllDeviceInfos.get(logicalAddress);
548        }
549    }
550
551    /**
552     * Called when a device is newly added or a new device is detected.
553     *
554     * @param info device info of a new device.
555     */
556    @ServiceThreadOnly
557    final void addCecDevice(HdmiCecDeviceInfo info) {
558        assertRunOnServiceThread();
559        addDeviceInfo(info);
560        if (info.getLogicalAddress() == mAddress) {
561            // The addition of TV device itself should not be notified.
562            return;
563        }
564        mService.invokeDeviceEventListeners(info, true);
565    }
566
567    /**
568     * Called when a device is removed or removal of device is detected.
569     *
570     * @param address a logical address of a device to be removed
571     */
572    @ServiceThreadOnly
573    final void removeCecDevice(int address) {
574        assertRunOnServiceThread();
575        HdmiCecDeviceInfo info = removeDeviceInfo(address);
576        mCecMessageCache.flushMessagesFrom(address);
577        mService.invokeDeviceEventListeners(info, false);
578    }
579
580    /**
581     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
582     * the given routing path. CEC devices use routing path for its physical address to
583     * describe the hierarchy of the devices in the network.
584     *
585     * @param path routing path or physical address
586     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
587     */
588    @ServiceThreadOnly
589    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
590        assertRunOnServiceThread();
591        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
592            if (info.getPhysicalAddress() == path) {
593                return info;
594            }
595        }
596        return null;
597    }
598
599    /**
600     * Whether a device of the specified physical address and logical address exists
601     * in a device info list. However, both are minimal condition and it could
602     * be different device from the original one.
603     *
604     * @param physicalAddress physical address of a device to be searched
605     * @param logicalAddress logical address of a device to be searched
606     * @return true if exist; otherwise false
607     */
608    @ServiceThreadOnly
609    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
610        assertRunOnServiceThread();
611        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
612        if (device == null) {
613            return false;
614        }
615        return device.getPhysicalAddress() == physicalAddress;
616    }
617
618    @Override
619    @ServiceThreadOnly
620    void onHotplug(int portNo, boolean connected) {
621        assertRunOnServiceThread();
622
623        // Tv device will have permanent HotplugDetectionAction.
624        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
625        if (!hotplugActions.isEmpty()) {
626            // Note that hotplug action is single action running on a machine.
627            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
628            hotplugActions.get(0).pollAllDevicesNow();
629        }
630    }
631}
632