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