HdmiCecLocalDeviceTv.java revision 79c58a4b97f27ede6a1b680d2fece9c2a0edf7b7
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.hdmi;
18
19import android.hardware.hdmi.HdmiCec;
20import android.hardware.hdmi.HdmiCecDeviceInfo;
21import android.hardware.hdmi.HdmiCecMessage;
22import android.hardware.hdmi.IHdmiControlCallback;
23import android.os.RemoteException;
24import android.util.Slog;
25import android.util.SparseArray;
26
27import com.android.internal.annotations.GuardedBy;
28import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
29
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.List;
33import java.util.Locale;
34
35/**
36 * Represent a logical device of type TV residing in Android system.
37 */
38final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
39    private static final String TAG = "HdmiCecLocalDeviceTv";
40
41    // Whether ARC is "enabled" or not.
42    @GuardedBy("mLock")
43    private boolean mArcStatusEnabled = false;
44
45    @GuardedBy("mLock")
46    // Whether SystemAudioMode is "On" or not.
47    private boolean mSystemAudioMode;
48
49    // Map-like container of all cec devices including local ones.
50    // A logical address of device is used as key of container.
51    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
52
53    HdmiCecLocalDeviceTv(HdmiControlService service) {
54        super(service, HdmiCec.DEVICE_TV);
55    }
56
57    @Override
58    protected void onAddressAllocated(int logicalAddress) {
59        assertRunOnServiceThread();
60        // TODO: vendor-specific initialization here.
61
62        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
63                mAddress, mService.getPhysicalAddress(), mDeviceType));
64        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
65                mAddress, mService.getVendorId()));
66
67        launchDeviceDiscovery();
68        // TODO: Start routing control action, device discovery action.
69    }
70
71    /**
72     * Performs the action 'device select', or 'one touch play' initiated by TV.
73     *
74     * @param targetAddress logical address of the device to select
75     * @param callback callback object to report the result with
76     */
77    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
78        assertRunOnServiceThread();
79        HdmiCecDeviceInfo targetDevice = mService.getDeviceInfo(targetAddress);
80        if (targetDevice == null) {
81            invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE);
82            return;
83        }
84        removeAction(DeviceSelectAction.class);
85        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
86    }
87
88    private static void invokeCallback(IHdmiControlCallback callback, int result) {
89        try {
90            callback.onComplete(result);
91        } catch (RemoteException e) {
92            Slog.e(TAG, "Invoking callback failed:" + e);
93        }
94    }
95
96    @Override
97    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
98        assertRunOnServiceThread();
99        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
100                mAddress, Locale.getDefault().getISO3Language());
101        // TODO: figure out how to handle failed to get language code.
102        if (command != null) {
103            mService.sendCecCommand(command);
104        } else {
105            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
106        }
107        return true;
108    }
109
110    @Override
111    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
112        assertRunOnServiceThread();
113        // Ignore if [Device Discovery Action] is going on.
114        if (hasAction(DeviceDiscoveryAction.class)) {
115            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
116                    + "because Device Discovery Action is on-going:" + message);
117            return true;
118        }
119
120        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
121        int logicalAddress = message.getSource();
122
123        // If it is a new device and connected to the tail of active path,
124        // it's required to change routing path.
125        boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress)
126                && isTailOfActivePath(physicalAddress);
127        addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress,
128                requireRoutingChange));
129        return true;
130    }
131
132    @Override
133    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
134        assertRunOnServiceThread();
135        List<VendorSpecificAction> actions = Collections.emptyList();
136        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
137
138        // We assume that there can be multiple vendor-specific command actions running
139        // at the same time. Pass the message to each action to see if one of them needs it.
140        for (VendorSpecificAction action : actions) {
141            if (action.processCommand(message)) {
142                return true;
143            }
144        }
145        // Handle the message here if it is not already consumed by one of the running actions.
146        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
147        // vendor-specific action:
148        //
149        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
150        //
151        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
152        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
153                message.getDestination(), message.getSource(), message.getOpcode(),
154                HdmiConstants.ABORT_REFUSED));
155        return true;
156    }
157
158    private void launchDeviceDiscovery() {
159        assertRunOnServiceThread();
160        clearDeviceInfoList();
161        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
162                new DeviceDiscoveryCallback() {
163                    @Override
164                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
165                        for (HdmiCecDeviceInfo info : deviceInfos) {
166                            addCecDevice(info);
167                        }
168
169                        // Since we removed all devices when it's start and
170                        // device discovery action does not poll local devices,
171                        // we should put device info of local device manually here
172                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
173                            addCecDevice(device.getDeviceInfo());
174                        }
175
176                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
177                    }
178                });
179        addAndStartAction(action);
180    }
181
182    // Clear all device info.
183    private void clearDeviceInfoList() {
184        assertRunOnServiceThread();
185        mDeviceInfos.clear();
186    }
187
188    void setSystemAudioMode(boolean on) {
189        synchronized (mLock) {
190            if (on != mSystemAudioMode) {
191                mSystemAudioMode = on;
192                // TODO: Need to set the preference for SystemAudioMode.
193                // TODO: Need to handle the notification of changing the mode and
194                // to identify the notification should be handled in the service or TvSettings.
195            }
196        }
197    }
198
199    boolean getSystemAudioMode() {
200        synchronized (mLock) {
201            assertRunOnServiceThread();
202            return mSystemAudioMode;
203        }
204    }
205
206    /**
207     * Change ARC status into the given {@code enabled} status.
208     *
209     * @return {@code true} if ARC was in "Enabled" status
210     */
211    boolean setArcStatus(boolean enabled) {
212        synchronized (mLock) {
213            boolean oldStatus = mArcStatusEnabled;
214            // 1. Enable/disable ARC circuit.
215            mService.setAudioReturnChannel(enabled);
216
217            // TODO: notify arc mode change to AudioManager.
218
219            // 2. Update arc status;
220            mArcStatusEnabled = enabled;
221            return oldStatus;
222        }
223    }
224
225    /**
226     * Returns whether ARC is enabled or not.
227     */
228    boolean getArcStatus() {
229        synchronized (mLock) {
230            return mArcStatusEnabled;
231        }
232    }
233
234    void setAudioStatus(boolean mute, int volume) {
235        mService.setAudioStatus(mute, volume);
236    }
237
238    @Override
239    protected boolean handleInitiateArc(HdmiCecMessage message) {
240        assertRunOnServiceThread();
241        // In case where <Initiate Arc> is started by <Request ARC Initiation>
242        // need to clean up RequestArcInitiationAction.
243        removeAction(RequestArcInitiationAction.class);
244        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
245                message.getSource(), true);
246        addAndStartAction(action);
247        return true;
248    }
249
250    @Override
251    protected boolean handleTerminateArc(HdmiCecMessage message) {
252        assertRunOnServiceThread();
253        // In case where <Terminate Arc> is started by <Request ARC Termination>
254        // need to clean up RequestArcInitiationAction.
255        // TODO: check conditions of power status by calling is_connected api
256        // to be added soon.
257        removeAction(RequestArcTerminationAction.class);
258        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
259                message.getSource(), false);
260        addAndStartAction(action);
261        return true;
262    }
263
264    @Override
265    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
266        assertRunOnServiceThread();
267        if (!isMessageForSystemAudio(message)) {
268            return false;
269        }
270        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
271                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message));
272        addAndStartAction(action);
273        return true;
274    }
275
276    @Override
277    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
278        assertRunOnServiceThread();
279        if (!isMessageForSystemAudio(message)) {
280            return false;
281        }
282        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
283        return true;
284    }
285
286    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
287        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
288                || message.getDestination() != HdmiCec.ADDR_TV
289                || getAvrDeviceInfo() == null) {
290            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
291            return false;
292        }
293        return true;
294    }
295
296    /**
297     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
298     * logical address as new device info's.
299     *
300     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
301     *
302     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
303     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
304     *         that has the same logical address as new one has.
305     */
306    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
307        assertRunOnServiceThread();
308        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
309        if (oldDeviceInfo != null) {
310            removeDeviceInfo(deviceInfo.getLogicalAddress());
311        }
312        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
313        return oldDeviceInfo;
314    }
315
316    /**
317     * Remove a device info corresponding to the given {@code logicalAddress}.
318     * It returns removed {@link HdmiCecDeviceInfo} if exists.
319     *
320     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
321     *
322     * @param logicalAddress logical address of device to be removed
323     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
324     */
325    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
326        assertRunOnServiceThread();
327        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
328        if (deviceInfo != null) {
329            mDeviceInfos.remove(logicalAddress);
330        }
331        return deviceInfo;
332    }
333
334    /**
335     * Return a list of all {@link HdmiCecDeviceInfo}.
336     *
337     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
338     */
339    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
340        assertRunOnServiceThread();
341        if (includelLocalDevice) {
342                return HdmiUtils.sparseArrayToList(mDeviceInfos);
343        } else {
344
345            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
346            for (int i = 0; i < mDeviceInfos.size(); ++i) {
347                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
348                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
349                    infoList.add(info);
350                }
351            }
352            return infoList;
353        }
354    }
355
356    private boolean isLocalDeviceAddress(int address) {
357        assertRunOnServiceThread();
358        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
359            if (device.isAddressOf(address)) {
360                return true;
361            }
362        }
363        return false;
364    }
365
366    /**
367     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
368     *
369     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
370     *
371     * @param logicalAddress logical address to be retrieved
372     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
373     *         Returns null if no logical address matched
374     */
375    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
376        assertRunOnServiceThread();
377        return mDeviceInfos.get(logicalAddress);
378    }
379
380    HdmiCecDeviceInfo getAvrDeviceInfo() {
381        assertRunOnServiceThread();
382        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
383    }
384
385    /**
386     * Called when a device is newly added or a new device is detected.
387     *
388     * @param info device info of a new device.
389     */
390    final void addCecDevice(HdmiCecDeviceInfo info) {
391        assertRunOnServiceThread();
392        addDeviceInfo(info);
393
394        // TODO: announce new device detection.
395    }
396
397    /**
398     * Called when a device is removed or removal of device is detected.
399     *
400     * @param address a logical address of a device to be removed
401     */
402    final void removeCecDevice(int address) {
403        assertRunOnServiceThread();
404        removeDeviceInfo(address);
405        mCecMessageCache.flushMessagesFrom(address);
406
407        // TODO: announce a device removal.
408    }
409
410    /**
411     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
412     * the given routing path. CEC devices use routing path for its physical address to
413     * describe the hierarchy of the devices in the network.
414     *
415     * @param path routing path or physical address
416     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
417     */
418    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
419        assertRunOnServiceThread();
420        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
421            if (info.getPhysicalAddress() == path) {
422                return info;
423            }
424        }
425        return null;
426    }
427
428    /**
429     * Whether a device of the specified physical address and logical address exists
430     * in a device info list. However, both are minimal condition and it could
431     * be different device from the original one.
432     *
433     * @param physicalAddress physical address of a device to be searched
434     * @param logicalAddress logical address of a device to be searched
435     * @return true if exist; otherwise false
436     */
437    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
438        assertRunOnServiceThread();
439        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
440        if (device == null) {
441            return false;
442        }
443        return device.getPhysicalAddress() == physicalAddress;
444    }
445
446    @Override
447    void onHotplug(int portNo, boolean connected) {
448        assertRunOnServiceThread();
449        // TODO: delegate onHotplug event to each local device.
450
451        // Tv device will have permanent HotplugDetectionAction.
452        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
453        if (!hotplugActions.isEmpty()) {
454            // Note that hotplug action is single action running on a machine.
455            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
456            hotplugActions.get(0).pollAllDevicesNow();
457        }
458    }
459}
460