HdmiCecLocalDevice.java revision 210d73df0b77b4c8be67ecc92afb238dc8c7ccfa
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.os.Looper;
23import android.util.Slog;
24
25import com.android.internal.annotations.GuardedBy;
26import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
27
28import java.util.ArrayList;
29import java.util.Iterator;
30import java.util.LinkedList;
31import java.util.List;
32
33/**
34 * Class that models a logical CEC device hosted in this system. Handles initialization,
35 * CEC commands that call for actions customized per device type.
36 */
37abstract class HdmiCecLocalDevice {
38    private static final String TAG = "HdmiCecLocalDevice";
39
40    protected final HdmiControlService mService;
41    protected final int mDeviceType;
42    protected int mAddress;
43    protected int mPreferredAddress;
44    protected HdmiCecDeviceInfo mDeviceInfo;
45
46    // Logical address of the active source.
47    @GuardedBy("mLock")
48    private int mActiveSource;
49
50    // Active routing path. Physical address of the active source but not all the time, such as
51    // when the new active source does not claim itself to be one. Note that we don't keep
52    // the active port id (or active input) since it can be gotten by {@link #pathToPortId(int)}.
53    @GuardedBy("mLock")
54    private int mActiveRoutingPath;
55
56    // Set to true while the service is in normal mode. While set to false, no input change is
57    // allowed. Used for situations where input change can confuse users such as channel auto-scan,
58    // system upgrade, etc., a.k.a. "prohibit mode".
59    @GuardedBy("mLock")
60    private boolean mInputChangeEnabled;
61
62    protected final HdmiCecMessageCache mCecMessageCache = new HdmiCecMessageCache();
63    protected final Object mLock;
64
65    // A collection of FeatureAction.
66    // Note that access to this collection should happen in service thread.
67    private final LinkedList<FeatureAction> mActions = new LinkedList<>();
68
69    protected HdmiCecLocalDevice(HdmiControlService service, int deviceType) {
70        mService = service;
71        mDeviceType = deviceType;
72        mAddress = HdmiCec.ADDR_UNREGISTERED;
73        mLock = service.getServiceLock();
74
75        // TODO: Get control flag from persistent storage
76        mInputChangeEnabled = true;
77    }
78
79    // Factory method that returns HdmiCecLocalDevice of corresponding type.
80    static HdmiCecLocalDevice create(HdmiControlService service, int deviceType) {
81        switch (deviceType) {
82        case HdmiCec.DEVICE_TV:
83            return new HdmiCecLocalDeviceTv(service);
84        case HdmiCec.DEVICE_PLAYBACK:
85            return new HdmiCecLocalDevicePlayback(service);
86        default:
87            return null;
88        }
89    }
90
91    @ServiceThreadOnly
92    void init() {
93        assertRunOnServiceThread();
94        mPreferredAddress = HdmiCec.ADDR_UNREGISTERED;
95        // TODO: load preferred address from permanent storage.
96    }
97
98    /**
99     * Called once a logical address of the local device is allocated.
100     */
101    protected abstract void onAddressAllocated(int logicalAddress);
102
103    /**
104     * Dispatch incoming message.
105     *
106     * @param message incoming message
107     * @return true if consumed a message; otherwise, return false.
108     */
109    @ServiceThreadOnly
110    final boolean dispatchMessage(HdmiCecMessage message) {
111        assertRunOnServiceThread();
112        int dest = message.getDestination();
113        if (dest != mAddress && dest != HdmiCec.ADDR_BROADCAST) {
114            return false;
115        }
116        // Cache incoming message. Note that it caches only white-listed one.
117        mCecMessageCache.cacheMessage(message);
118        return onMessage(message);
119    }
120
121    @ServiceThreadOnly
122    protected final boolean onMessage(HdmiCecMessage message) {
123        assertRunOnServiceThread();
124        if (dispatchMessageToAction(message)) {
125            return true;
126        }
127        switch (message.getOpcode()) {
128            case HdmiCec.MESSAGE_ACTIVE_SOURCE:
129                return handleActiveSource(message);
130            case HdmiCec.MESSAGE_INACTIVE_SOURCE:
131                return handleInactiveSource(message);
132            case HdmiCec.MESSAGE_REQUEST_ACTIVE_SOURCE:
133                return handleRequestActiveSource(message);
134            case HdmiCec.MESSAGE_GET_MENU_LANGUAGE:
135                return handleGetMenuLanguage(message);
136            case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS:
137                return handleGivePhysicalAddress();
138            case HdmiCec.MESSAGE_GIVE_OSD_NAME:
139                return handleGiveOsdName(message);
140            case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID:
141                return handleGiveDeviceVendorId();
142            case HdmiCec.MESSAGE_GET_CEC_VERSION:
143                return handleGetCecVersion(message);
144            case HdmiCec.MESSAGE_REPORT_PHYSICAL_ADDRESS:
145                return handleReportPhysicalAddress(message);
146            case HdmiCec.MESSAGE_ROUTING_CHANGE:
147                return handleRoutingChange(message);
148            case HdmiCec.MESSAGE_INITIATE_ARC:
149                return handleInitiateArc(message);
150            case HdmiCec.MESSAGE_TERMINATE_ARC:
151                return handleTerminateArc(message);
152            case HdmiCec.MESSAGE_SET_SYSTEM_AUDIO_MODE:
153                return handleSetSystemAudioMode(message);
154            case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
155                return handleSystemAudioModeStatus(message);
156            case HdmiCec.MESSAGE_REPORT_AUDIO_STATUS:
157                return handleReportAudioStatus(message);
158            case HdmiCec.MESSAGE_STANDBY:
159                return handleStandby(message);
160            case HdmiCec.MESSAGE_TEXT_VIEW_ON:
161                return handleTextViewOn(message);
162            case HdmiCec.MESSAGE_IMAGE_VIEW_ON:
163                return handleImageViewOn(message);
164            case HdmiCec.MESSAGE_USER_CONTROL_PRESSED:
165                return handleUserControlPressed(message);
166            case HdmiCec.MESSAGE_SET_STREAM_PATH:
167                return handleSetStreamPath(message);
168            case HdmiCec.MESSAGE_GIVE_DEVICE_POWER_STATUS:
169                return handleGiveDevicePowerStatus(message);
170            default:
171                return false;
172        }
173    }
174
175    @ServiceThreadOnly
176    private boolean dispatchMessageToAction(HdmiCecMessage message) {
177        assertRunOnServiceThread();
178        for (FeatureAction action : mActions) {
179            if (action.processCommand(message)) {
180                return true;
181            }
182        }
183        return false;
184    }
185
186    @ServiceThreadOnly
187    protected boolean handleGivePhysicalAddress() {
188        assertRunOnServiceThread();
189
190        int physicalAddress = mService.getPhysicalAddress();
191        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
192                mAddress, physicalAddress, mDeviceType);
193        mService.sendCecCommand(cecMessage);
194        return true;
195    }
196
197    @ServiceThreadOnly
198    protected boolean handleGiveDeviceVendorId() {
199        assertRunOnServiceThread();
200        int vendorId = mService.getVendorId();
201        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
202                mAddress, vendorId);
203        mService.sendCecCommand(cecMessage);
204        return true;
205    }
206
207    @ServiceThreadOnly
208    protected boolean handleGetCecVersion(HdmiCecMessage message) {
209        assertRunOnServiceThread();
210        int version = mService.getCecVersion();
211        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
212                message.getSource(), version);
213        mService.sendCecCommand(cecMessage);
214        return true;
215    }
216
217    @ServiceThreadOnly
218    protected boolean handleActiveSource(HdmiCecMessage message) {
219        return false;
220    }
221
222    @ServiceThreadOnly
223    protected boolean handleInactiveSource(HdmiCecMessage message) {
224        return false;
225    }
226
227    @ServiceThreadOnly
228    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
229        return false;
230    }
231
232    @ServiceThreadOnly
233    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
234        assertRunOnServiceThread();
235        Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
236        mService.sendCecCommand(
237                HdmiCecMessageBuilder.buildFeatureAbortCommand(mAddress,
238                        message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
239                        HdmiConstants.ABORT_UNRECOGNIZED_MODE));
240        return true;
241    }
242
243    @ServiceThreadOnly
244    protected boolean handleGiveOsdName(HdmiCecMessage message) {
245        assertRunOnServiceThread();
246        // Note that since this method is called after logical address allocation is done,
247        // mDeviceInfo should not be null.
248        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
249                mAddress, message.getSource(), mDeviceInfo.getDisplayName());
250        if (cecMessage != null) {
251            mService.sendCecCommand(cecMessage);
252        } else {
253            Slog.w(TAG, "Failed to build <Get Osd Name>:" + mDeviceInfo.getDisplayName());
254        }
255        return true;
256    }
257
258    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
259        return false;
260    }
261
262    protected boolean handleRoutingChange(HdmiCecMessage message) {
263        return false;
264    }
265
266    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
267        return false;
268    }
269
270    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
271        return false;
272    }
273
274    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
275        return false;
276    }
277
278    protected boolean handleTerminateArc(HdmiCecMessage message) {
279        return false;
280    }
281
282    protected boolean handleInitiateArc(HdmiCecMessage message) {
283        return false;
284    }
285
286    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
287        return false;
288    }
289
290    @ServiceThreadOnly
291    protected boolean handleStandby(HdmiCecMessage message) {
292        assertRunOnServiceThread();
293        // Seq #12
294        if (mService.isControlEnabled() && !isInPresetInstallationMode()
295                && mService.isPowerOnOrTransient()) {
296            mService.standby();
297            return true;
298        }
299        return false;
300    }
301
302    @ServiceThreadOnly
303    protected boolean handleUserControlPressed(HdmiCecMessage message) {
304        assertRunOnServiceThread();
305        if (mService.isPowerOnOrTransient() && isPowerOffOrToggleCommand(message)) {
306            mService.standby();
307            return true;
308        } else if (mService.isPowerStandbyOrTransient() && isPowerOnOrToggleCommand(message)) {
309            mService.wakeUp();
310            return true;
311        }
312        return false;
313    }
314
315    private static boolean isPowerOnOrToggleCommand(HdmiCecMessage message) {
316        byte[] params = message.getParams();
317        return message.getOpcode() == HdmiCec.MESSAGE_USER_CONTROL_PRESSED && params.length == 1
318                && (params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER
319                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_ON_FUNCTION
320                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
321    }
322
323    private static boolean isPowerOffOrToggleCommand(HdmiCecMessage message) {
324        byte[] params = message.getParams();
325        return message.getOpcode() == HdmiCec.MESSAGE_USER_CONTROL_PRESSED && params.length == 1
326                && (params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER
327                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_OFF_FUNCTION
328                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
329    }
330
331    protected boolean handleTextViewOn(HdmiCecMessage message) {
332        return false;
333    }
334
335    protected boolean handleImageViewOn(HdmiCecMessage message) {
336        return false;
337    }
338
339    protected boolean handleSetStreamPath(HdmiCecMessage message) {
340        return false;
341    }
342
343    protected boolean handleGiveDevicePowerStatus(HdmiCecMessage message) {
344        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPowerStatus(
345                mAddress, message.getSource(), mService.getPowerStatus()));
346        return true;
347    }
348
349    @ServiceThreadOnly
350    final void handleAddressAllocated(int logicalAddress) {
351        assertRunOnServiceThread();
352        mAddress = mPreferredAddress = logicalAddress;
353        onAddressAllocated(logicalAddress);
354    }
355
356    @ServiceThreadOnly
357    HdmiCecDeviceInfo getDeviceInfo() {
358        assertRunOnServiceThread();
359        return mDeviceInfo;
360    }
361
362    @ServiceThreadOnly
363    void setDeviceInfo(HdmiCecDeviceInfo info) {
364        assertRunOnServiceThread();
365        mDeviceInfo = info;
366    }
367
368    // Returns true if the logical address is same as the argument.
369    @ServiceThreadOnly
370    boolean isAddressOf(int addr) {
371        assertRunOnServiceThread();
372        return addr == mAddress;
373    }
374
375    // Resets the logical address to unregistered(15), meaning the logical device is invalid.
376    @ServiceThreadOnly
377    void clearAddress() {
378        assertRunOnServiceThread();
379        mAddress = HdmiCec.ADDR_UNREGISTERED;
380    }
381
382    @ServiceThreadOnly
383    void setPreferredAddress(int addr) {
384        assertRunOnServiceThread();
385        mPreferredAddress = addr;
386    }
387
388    @ServiceThreadOnly
389    int getPreferredAddress() {
390        assertRunOnServiceThread();
391        return mPreferredAddress;
392    }
393
394    @ServiceThreadOnly
395    void addAndStartAction(final FeatureAction action) {
396        assertRunOnServiceThread();
397        if (mService.isPowerStandbyOrTransient()) {
398            Slog.w(TAG, "Skip the action during Standby: " + action);
399            return;
400        }
401        mActions.add(action);
402        action.start();
403    }
404
405    // See if we have an action of a given type in progress.
406    @ServiceThreadOnly
407    <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
408        assertRunOnServiceThread();
409        for (FeatureAction action : mActions) {
410            if (action.getClass().equals(clazz)) {
411                return true;
412            }
413        }
414        return false;
415    }
416
417    // Returns all actions matched with given class type.
418    @ServiceThreadOnly
419    <T extends FeatureAction> List<T> getActions(final Class<T> clazz) {
420        assertRunOnServiceThread();
421        ArrayList<T> actions = new ArrayList<>();
422        for (FeatureAction action : mActions) {
423            if (action.getClass().equals(clazz)) {
424                actions.add((T) action);
425            }
426        }
427        return actions;
428    }
429
430    /**
431     * Remove the given {@link FeatureAction} object from the action queue.
432     *
433     * @param action {@link FeatureAction} to remove
434     */
435    @ServiceThreadOnly
436    void removeAction(final FeatureAction action) {
437        assertRunOnServiceThread();
438        mActions.remove(action);
439        checkIfPendingActionsCleared();
440    }
441
442    // Remove all actions matched with the given Class type.
443    @ServiceThreadOnly
444    <T extends FeatureAction> void removeAction(final Class<T> clazz) {
445        assertRunOnServiceThread();
446        removeActionExcept(clazz, null);
447    }
448
449    // Remove all actions matched with the given Class type besides |exception|.
450    @ServiceThreadOnly
451    <T extends FeatureAction> void removeActionExcept(final Class<T> clazz,
452            final FeatureAction exception) {
453        assertRunOnServiceThread();
454        Iterator<FeatureAction> iter = mActions.iterator();
455        while (iter.hasNext()) {
456            FeatureAction action = iter.next();
457            if (action != exception && action.getClass().equals(clazz)) {
458                action.clear();
459                mActions.remove(action);
460            }
461        }
462        checkIfPendingActionsCleared();
463    }
464
465    protected void checkIfPendingActionsCleared() {
466        if (mActions.isEmpty()) {
467            mService.onPendingActionsCleared();
468        }
469    }
470    protected void assertRunOnServiceThread() {
471        if (Looper.myLooper() != mService.getServiceLooper()) {
472            throw new IllegalStateException("Should run on service thread.");
473        }
474    }
475
476    /**
477     * Called when a hot-plug event issued.
478     *
479     * @param portId id of port where a hot-plug event happened
480     * @param connected whether to connected or not on the event
481     */
482    void onHotplug(int portId, boolean connected) {
483    }
484
485    final HdmiControlService getService() {
486        return mService;
487    }
488
489    @ServiceThreadOnly
490    final boolean isConnectedToArcPort(int path) {
491        assertRunOnServiceThread();
492        return mService.isConnectedToArcPort(path);
493    }
494
495    int getActiveSource() {
496        synchronized (mLock) {
497            return mActiveSource;
498        }
499    }
500
501    void setActiveSource(int source) {
502        synchronized (mLock) {
503            mActiveSource = source;
504        }
505    }
506
507    int getActivePath() {
508        synchronized (mLock) {
509            return mActiveRoutingPath;
510        }
511    }
512
513    void setActivePath(int path) {
514        synchronized (mLock) {
515            mActiveRoutingPath = path;
516        }
517    }
518
519    /**
520     * Returns the ID of the active HDMI port. The active port is the one that has the active
521     * routing path connected to it directly or indirectly under the device hierarchy.
522     */
523    int getActivePortId() {
524        synchronized (mLock) {
525            return mService.pathToPortId(mActiveRoutingPath);
526        }
527    }
528
529    /**
530     * Update the active port.
531     *
532     * @param portId the new active port id
533     */
534    void setActivePortId(int portId) {
535        synchronized (mLock) {
536            // We update active routing path instead, since we get the active port id from
537            // the active routing path.
538            mActiveRoutingPath = mService.portIdToPath(portId);
539        }
540    }
541
542    void updateActiveDevice(int logicalAddress, int physicalAddress) {
543        synchronized (mLock) {
544            mActiveSource = logicalAddress;
545            mActiveRoutingPath = physicalAddress;
546        }
547    }
548
549    void setInputChangeEnabled(boolean enabled) {
550        synchronized (mLock) {
551            mInputChangeEnabled = enabled;
552        }
553    }
554
555    boolean isInPresetInstallationMode() {
556        // TODO: Change this to check the right flag.
557        synchronized (mLock) {
558            return !mInputChangeEnabled;
559        }
560    }
561
562    @ServiceThreadOnly
563    HdmiCecMessageCache getCecMessageCache() {
564        assertRunOnServiceThread();
565        return mCecMessageCache;
566    }
567
568    @ServiceThreadOnly
569    int pathToPortId(int newPath) {
570        assertRunOnServiceThread();
571        return mService.pathToPortId(newPath);
572    }
573
574    /**
575     * Called when the system started transition to standby mode.
576     *
577     * @param initiatedByCec true if this power sequence is initiated
578     *         by the reception the CEC messages like <StandBy>
579     */
580    protected void onTransitionToStandby(boolean initiatedByCec) {
581        // If there are no outstanding actions, we'll go to STANDBY state.
582        checkIfPendingActionsCleared();
583    }
584
585    /**
586     * Called when the system goes to standby mode.
587     *
588     * @param initiatedByCec true if this power sequence is initiated
589     *         by the reception the CEC messages like <StandBy>
590     */
591    protected void onStandBy(boolean initiatedByCec) {}
592}
593