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