HdmiCecLocalDevice.java revision b6591b8e5399099dc6b7693e0fc719b613aba89c
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.HdmiCecDeviceInfo;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.Message;
23import android.os.SystemProperties;
24import android.util.Slog;
25
26import com.android.internal.annotations.GuardedBy;
27import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
28
29import java.util.ArrayList;
30import java.util.Iterator;
31import java.util.LinkedList;
32import java.util.List;
33
34/**
35 * Class that models a logical CEC device hosted in this system. Handles initialization,
36 * CEC commands that call for actions customized per device type.
37 */
38abstract class HdmiCecLocalDevice {
39    private static final String TAG = "HdmiCecLocalDevice";
40
41    private static final int MSG_DISABLE_DEVICE_TIMEOUT = 1;
42    // Timeout in millisecond for device clean up (5s).
43    // Normal actions timeout is 2s but some of them would have several sequence of timeout.
44    private static final int DEVICE_CLEANUP_TIMEOUT = 5000;
45
46    protected final HdmiControlService mService;
47    protected final int mDeviceType;
48    protected int mAddress;
49    protected int mPreferredAddress;
50    protected HdmiCecDeviceInfo mDeviceInfo;
51
52    // Logical address of the active source.
53    @GuardedBy("mLock")
54    private int mActiveSource;
55
56    // Active routing path. Physical address of the active source but not all the time, such as
57    // when the new active source does not claim itself to be one. Note that we don't keep
58    // the active port id (or active input) since it can be gotten by {@link #pathToPortId(int)}.
59    @GuardedBy("mLock")
60    private int mActiveRoutingPath;
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    private final Handler mHandler = new Handler () {
70        @Override
71        public void handleMessage(Message msg) {
72            switch (msg.what) {
73                case MSG_DISABLE_DEVICE_TIMEOUT:
74                    handleDisableDeviceTimeout();
75                    break;
76            }
77        }
78    };
79
80    /**
81     * A callback interface to get notified when all pending action is cleared.
82     * It can be called when timeout happened.
83     */
84    interface PendingActionClearedCallback {
85        void onCleared(HdmiCecLocalDevice device);
86    }
87
88    protected PendingActionClearedCallback mPendingActionClearedCallback;
89
90    protected HdmiCecLocalDevice(HdmiControlService service, int deviceType) {
91        mService = service;
92        mDeviceType = deviceType;
93        mAddress = Constants.ADDR_UNREGISTERED;
94        mLock = service.getServiceLock();
95    }
96
97    // Factory method that returns HdmiCecLocalDevice of corresponding type.
98    static HdmiCecLocalDevice create(HdmiControlService service, int deviceType) {
99        switch (deviceType) {
100        case HdmiCecDeviceInfo.DEVICE_TV:
101            return new HdmiCecLocalDeviceTv(service);
102        case HdmiCecDeviceInfo.DEVICE_PLAYBACK:
103            return new HdmiCecLocalDevicePlayback(service);
104        default:
105            return null;
106        }
107    }
108
109    @ServiceThreadOnly
110    void init() {
111        assertRunOnServiceThread();
112        mPreferredAddress = getPreferredAddress();
113    }
114
115    /**
116     * Called once a logical address of the local device is allocated.
117     */
118    protected abstract void onAddressAllocated(int logicalAddress, boolean fromBootup);
119
120    /**
121     * Get the preferred logical address from system properties.
122     */
123    protected abstract int getPreferredAddress();
124
125    /**
126     * Set the preferred logical address to system properties.
127     */
128    protected abstract void setPreferredAddress(int addr);
129
130    /**
131     * Dispatch incoming message.
132     *
133     * @param message incoming message
134     * @return true if consumed a message; otherwise, return false.
135     */
136    @ServiceThreadOnly
137    final boolean dispatchMessage(HdmiCecMessage message) {
138        assertRunOnServiceThread();
139        int dest = message.getDestination();
140        if (dest != mAddress && dest != Constants.ADDR_BROADCAST) {
141            return false;
142        }
143        // Cache incoming message. Note that it caches only white-listed one.
144        mCecMessageCache.cacheMessage(message);
145        return onMessage(message);
146    }
147
148    @ServiceThreadOnly
149    protected final boolean onMessage(HdmiCecMessage message) {
150        assertRunOnServiceThread();
151        if (dispatchMessageToAction(message)) {
152            return true;
153        }
154        switch (message.getOpcode()) {
155            case Constants.MESSAGE_ACTIVE_SOURCE:
156                return handleActiveSource(message);
157            case Constants.MESSAGE_INACTIVE_SOURCE:
158                return handleInactiveSource(message);
159            case Constants.MESSAGE_REQUEST_ACTIVE_SOURCE:
160                return handleRequestActiveSource(message);
161            case Constants.MESSAGE_GET_MENU_LANGUAGE:
162                return handleGetMenuLanguage(message);
163            case Constants.MESSAGE_GIVE_PHYSICAL_ADDRESS:
164                return handleGivePhysicalAddress();
165            case Constants.MESSAGE_GIVE_OSD_NAME:
166                return handleGiveOsdName(message);
167            case Constants.MESSAGE_GIVE_DEVICE_VENDOR_ID:
168                return handleGiveDeviceVendorId();
169            case Constants.MESSAGE_GET_CEC_VERSION:
170                return handleGetCecVersion(message);
171            case Constants.MESSAGE_REPORT_PHYSICAL_ADDRESS:
172                return handleReportPhysicalAddress(message);
173            case Constants.MESSAGE_ROUTING_CHANGE:
174                return handleRoutingChange(message);
175            case Constants.MESSAGE_INITIATE_ARC:
176                return handleInitiateArc(message);
177            case Constants.MESSAGE_TERMINATE_ARC:
178                return handleTerminateArc(message);
179            case Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE:
180                return handleSetSystemAudioMode(message);
181            case Constants.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
182                return handleSystemAudioModeStatus(message);
183            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
184                return handleReportAudioStatus(message);
185            case Constants.MESSAGE_STANDBY:
186                return handleStandby(message);
187            case Constants.MESSAGE_TEXT_VIEW_ON:
188                return handleTextViewOn(message);
189            case Constants.MESSAGE_IMAGE_VIEW_ON:
190                return handleImageViewOn(message);
191            case Constants.MESSAGE_USER_CONTROL_PRESSED:
192                return handleUserControlPressed(message);
193            case Constants.MESSAGE_SET_STREAM_PATH:
194                return handleSetStreamPath(message);
195            case Constants.MESSAGE_GIVE_DEVICE_POWER_STATUS:
196                return handleGiveDevicePowerStatus(message);
197            case Constants.MESSAGE_VENDOR_COMMAND:
198                return handleVendorCommand(message);
199            case Constants.MESSAGE_VENDOR_COMMAND_WITH_ID:
200                return handleVendorCommandWithId(message);
201            case Constants.MESSAGE_SET_OSD_NAME:
202                return handleSetOsdName(message);
203            case Constants.MESSAGE_RECORD_TV_SCREEN:
204                return handleRecordTvScreen(message);
205            default:
206                return false;
207        }
208    }
209
210    @ServiceThreadOnly
211    private boolean dispatchMessageToAction(HdmiCecMessage message) {
212        assertRunOnServiceThread();
213        for (FeatureAction action : mActions) {
214            if (action.processCommand(message)) {
215                return true;
216            }
217        }
218        return false;
219    }
220
221    @ServiceThreadOnly
222    protected boolean handleGivePhysicalAddress() {
223        assertRunOnServiceThread();
224
225        int physicalAddress = mService.getPhysicalAddress();
226        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
227                mAddress, physicalAddress, mDeviceType);
228        mService.sendCecCommand(cecMessage);
229        return true;
230    }
231
232    @ServiceThreadOnly
233    protected boolean handleGiveDeviceVendorId() {
234        assertRunOnServiceThread();
235        int vendorId = mService.getVendorId();
236        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
237                mAddress, vendorId);
238        mService.sendCecCommand(cecMessage);
239        return true;
240    }
241
242    @ServiceThreadOnly
243    protected boolean handleGetCecVersion(HdmiCecMessage message) {
244        assertRunOnServiceThread();
245        int version = mService.getCecVersion();
246        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
247                message.getSource(), version);
248        mService.sendCecCommand(cecMessage);
249        return true;
250    }
251
252    @ServiceThreadOnly
253    protected boolean handleActiveSource(HdmiCecMessage message) {
254        return false;
255    }
256
257    @ServiceThreadOnly
258    protected boolean handleInactiveSource(HdmiCecMessage message) {
259        return false;
260    }
261
262    @ServiceThreadOnly
263    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
264        return false;
265    }
266
267    @ServiceThreadOnly
268    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
269        assertRunOnServiceThread();
270        Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
271        mService.sendCecCommand(
272                HdmiCecMessageBuilder.buildFeatureAbortCommand(mAddress,
273                        message.getSource(), Constants.MESSAGE_GET_MENU_LANGUAGE,
274                        Constants.ABORT_UNRECOGNIZED_MODE));
275        return true;
276    }
277
278    @ServiceThreadOnly
279    protected boolean handleGiveOsdName(HdmiCecMessage message) {
280        assertRunOnServiceThread();
281        // Note that since this method is called after logical address allocation is done,
282        // mDeviceInfo should not be null.
283        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
284                mAddress, message.getSource(), mDeviceInfo.getDisplayName());
285        if (cecMessage != null) {
286            mService.sendCecCommand(cecMessage);
287        } else {
288            Slog.w(TAG, "Failed to build <Get Osd Name>:" + mDeviceInfo.getDisplayName());
289        }
290        return true;
291    }
292
293    protected boolean handleRoutingChange(HdmiCecMessage message) {
294        return false;
295    }
296
297    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
298        return false;
299    }
300
301    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
302        return false;
303    }
304
305    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
306        return false;
307    }
308
309    protected boolean handleTerminateArc(HdmiCecMessage message) {
310        return false;
311    }
312
313    protected boolean handleInitiateArc(HdmiCecMessage message) {
314        return false;
315    }
316
317    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
318        return false;
319    }
320
321    @ServiceThreadOnly
322    protected boolean handleStandby(HdmiCecMessage message) {
323        assertRunOnServiceThread();
324        // Seq #12
325        if (mService.isControlEnabled() && !mService.isProhibitMode()
326                && mService.isPowerOnOrTransient()) {
327            mService.standby();
328            return true;
329        }
330        return false;
331    }
332
333    @ServiceThreadOnly
334    protected boolean handleUserControlPressed(HdmiCecMessage message) {
335        assertRunOnServiceThread();
336        if (mService.isPowerOnOrTransient() && isPowerOffOrToggleCommand(message)) {
337            mService.standby();
338            return true;
339        } else if (mService.isPowerStandbyOrTransient() && isPowerOnOrToggleCommand(message)) {
340            mService.wakeUp();
341            return true;
342        }
343        return false;
344    }
345
346    private static boolean isPowerOnOrToggleCommand(HdmiCecMessage message) {
347        byte[] params = message.getParams();
348        return message.getOpcode() == Constants.MESSAGE_USER_CONTROL_PRESSED
349                && (params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER
350                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_ON_FUNCTION
351                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
352    }
353
354    private static boolean isPowerOffOrToggleCommand(HdmiCecMessage message) {
355        byte[] params = message.getParams();
356        return message.getOpcode() == Constants.MESSAGE_USER_CONTROL_PRESSED
357                && (params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER
358                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_OFF_FUNCTION
359                        || params[0] == HdmiCecKeycode.CEC_KEYCODE_POWER_TOGGLE_FUNCTION);
360    }
361
362    protected boolean handleTextViewOn(HdmiCecMessage message) {
363        return false;
364    }
365
366    protected boolean handleImageViewOn(HdmiCecMessage message) {
367        return false;
368    }
369
370    protected boolean handleSetStreamPath(HdmiCecMessage message) {
371        return false;
372    }
373
374    protected boolean handleGiveDevicePowerStatus(HdmiCecMessage message) {
375        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPowerStatus(
376                mAddress, message.getSource(), mService.getPowerStatus()));
377        return true;
378    }
379
380    protected boolean handleVendorCommand(HdmiCecMessage message) {
381        mService.invokeVendorCommandListeners(mDeviceType, message.getSource(),
382                message.getParams(), false);
383        return true;
384    }
385
386    protected boolean handleVendorCommandWithId(HdmiCecMessage message) {
387        byte[] params = message.getParams();
388        int vendorId = HdmiUtils.threeBytesToInt(params);
389        if (vendorId == mService.getVendorId()) {
390            mService.invokeVendorCommandListeners(mDeviceType, message.getSource(), params, true);
391        } else if (message.getDestination() != Constants.ADDR_BROADCAST &&
392                message.getSource() != Constants.ADDR_UNREGISTERED) {
393            Slog.v(TAG, "Wrong direct vendor command. Replying with <Feature Abort>");
394            mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(mAddress,
395                    message.getSource(), Constants.MESSAGE_VENDOR_COMMAND_WITH_ID,
396                    Constants.ABORT_UNRECOGNIZED_MODE));
397        } else {
398            Slog.v(TAG, "Wrong broadcast vendor command. Ignoring");
399        }
400        return true;
401    }
402
403    protected boolean handleSetOsdName(HdmiCecMessage message) {
404        // The default behavior of <Set Osd Name> is doing nothing.
405        return true;
406    }
407
408    protected boolean handleRecordTvScreen(HdmiCecMessage message) {
409        // The default behavior of <Record TV Screen> is replying <Feature Abort> with "Refused".
410        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(mAddress,
411                message.getSource(), message.getOpcode(), Constants.ABORT_REFUSED));
412        return true;
413    }
414
415    @ServiceThreadOnly
416    final void handleAddressAllocated(int logicalAddress, boolean fromBootup) {
417        assertRunOnServiceThread();
418        mAddress = mPreferredAddress = logicalAddress;
419        onAddressAllocated(logicalAddress, fromBootup);
420        setPreferredAddress(logicalAddress);
421    }
422
423    @ServiceThreadOnly
424    HdmiCecDeviceInfo getDeviceInfo() {
425        assertRunOnServiceThread();
426        return mDeviceInfo;
427    }
428
429    @ServiceThreadOnly
430    void setDeviceInfo(HdmiCecDeviceInfo info) {
431        assertRunOnServiceThread();
432        mDeviceInfo = info;
433    }
434
435    // Returns true if the logical address is same as the argument.
436    @ServiceThreadOnly
437    boolean isAddressOf(int addr) {
438        assertRunOnServiceThread();
439        return addr == mAddress;
440    }
441
442    // Resets the logical address to unregistered(15), meaning the logical device is invalid.
443    @ServiceThreadOnly
444    void clearAddress() {
445        assertRunOnServiceThread();
446        mAddress = Constants.ADDR_UNREGISTERED;
447    }
448
449    @ServiceThreadOnly
450    void addAndStartAction(final FeatureAction action) {
451        assertRunOnServiceThread();
452        if (mService.isPowerStandbyOrTransient()) {
453            Slog.w(TAG, "Skip the action during Standby: " + action);
454            return;
455        }
456        mActions.add(action);
457        action.start();
458    }
459
460    // See if we have an action of a given type in progress.
461    @ServiceThreadOnly
462    <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
463        assertRunOnServiceThread();
464        for (FeatureAction action : mActions) {
465            if (action.getClass().equals(clazz)) {
466                return true;
467            }
468        }
469        return false;
470    }
471
472    // Returns all actions matched with given class type.
473    @ServiceThreadOnly
474    <T extends FeatureAction> List<T> getActions(final Class<T> clazz) {
475        assertRunOnServiceThread();
476        ArrayList<T> actions = new ArrayList<>();
477        for (FeatureAction action : mActions) {
478            if (action.getClass().equals(clazz)) {
479                actions.add((T) action);
480            }
481        }
482        return actions;
483    }
484
485    /**
486     * Remove the given {@link FeatureAction} object from the action queue.
487     *
488     * @param action {@link FeatureAction} to remove
489     */
490    @ServiceThreadOnly
491    void removeAction(final FeatureAction action) {
492        assertRunOnServiceThread();
493        action.finish(false);
494        mActions.remove(action);
495        checkIfPendingActionsCleared();
496    }
497
498    // Remove all actions matched with the given Class type.
499    @ServiceThreadOnly
500    <T extends FeatureAction> void removeAction(final Class<T> clazz) {
501        assertRunOnServiceThread();
502        removeActionExcept(clazz, null);
503    }
504
505    // Remove all actions matched with the given Class type besides |exception|.
506    @ServiceThreadOnly
507    <T extends FeatureAction> void removeActionExcept(final Class<T> clazz,
508            final FeatureAction exception) {
509        assertRunOnServiceThread();
510        Iterator<FeatureAction> iter = mActions.iterator();
511        while (iter.hasNext()) {
512            FeatureAction action = iter.next();
513            if (action != exception && action.getClass().equals(clazz)) {
514                action.finish(false);
515                iter.remove();
516            }
517        }
518        checkIfPendingActionsCleared();
519    }
520
521    protected void checkIfPendingActionsCleared() {
522        if (mActions.isEmpty() && mPendingActionClearedCallback != null) {
523            mPendingActionClearedCallback.onCleared(this);
524        }
525    }
526
527    protected void assertRunOnServiceThread() {
528        if (Looper.myLooper() != mService.getServiceLooper()) {
529            throw new IllegalStateException("Should run on service thread.");
530        }
531    }
532
533    /**
534     * Called when a hot-plug event issued.
535     *
536     * @param portId id of port where a hot-plug event happened
537     * @param connected whether to connected or not on the event
538     */
539    void onHotplug(int portId, boolean connected) {
540    }
541
542    final HdmiControlService getService() {
543        return mService;
544    }
545
546    @ServiceThreadOnly
547    final boolean isConnectedToArcPort(int path) {
548        assertRunOnServiceThread();
549        return mService.isConnectedToArcPort(path);
550    }
551
552    int getActiveSource() {
553        synchronized (mLock) {
554            return mActiveSource;
555        }
556    }
557
558    void setActiveSource(int source) {
559        synchronized (mLock) {
560            mActiveSource = source;
561        }
562    }
563
564    int getActivePath() {
565        synchronized (mLock) {
566            return mActiveRoutingPath;
567        }
568    }
569
570    void setActivePath(int path) {
571        synchronized (mLock) {
572            mActiveRoutingPath = path;
573        }
574    }
575
576    /**
577     * Returns the ID of the active HDMI port. The active port is the one that has the active
578     * routing path connected to it directly or indirectly under the device hierarchy.
579     */
580    int getActivePortId() {
581        synchronized (mLock) {
582            return mService.pathToPortId(mActiveRoutingPath);
583        }
584    }
585
586    /**
587     * Update the active port.
588     *
589     * @param portId the new active port id
590     */
591    void setActivePortId(int portId) {
592        synchronized (mLock) {
593            // We update active routing path instead, since we get the active port id from
594            // the active routing path.
595            mActiveRoutingPath = mService.portIdToPath(portId);
596        }
597    }
598
599    void updateActiveDevice(int logicalAddress, int physicalAddress) {
600        synchronized (mLock) {
601            mActiveSource = logicalAddress;
602            mActiveRoutingPath = physicalAddress;
603        }
604    }
605
606    @ServiceThreadOnly
607    HdmiCecMessageCache getCecMessageCache() {
608        assertRunOnServiceThread();
609        return mCecMessageCache;
610    }
611
612    @ServiceThreadOnly
613    int pathToPortId(int newPath) {
614        assertRunOnServiceThread();
615        return mService.pathToPortId(newPath);
616    }
617
618    /**
619     * Called when the system goes to standby mode.
620     *
621     * @param initiatedByCec true if this power sequence is initiated
622     *        by the reception the CEC messages like &lt;Standby&gt;
623     */
624    protected void onStandby(boolean initiatedByCec) {}
625
626    /**
627     * Disable device. {@code callback} is used to get notified when all pending
628     * actions are completed or timeout is issued.
629     *
630     * @param initiatedByCec true if this sequence is initiated
631     *        by the reception the CEC messages like &lt;Standby&gt;
632     * @param origialCallback callback interface to get notified when all pending actions are
633     *        cleared
634     */
635    protected void disableDevice(boolean initiatedByCec,
636            final PendingActionClearedCallback origialCallback) {
637        mPendingActionClearedCallback = new PendingActionClearedCallback() {
638            @Override
639            public void onCleared(HdmiCecLocalDevice device) {
640                mHandler.removeMessages(MSG_DISABLE_DEVICE_TIMEOUT);
641                origialCallback.onCleared(device);
642            }
643        };
644        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_DISABLE_DEVICE_TIMEOUT),
645                DEVICE_CLEANUP_TIMEOUT);
646    }
647
648    @ServiceThreadOnly
649    private void handleDisableDeviceTimeout() {
650        assertRunOnServiceThread();
651
652        // If all actions are not cleared in DEVICE_CLEANUP_TIMEOUT, enforce to finish them.
653        // onCleard will be called at the last action's finish method.
654        Iterator<FeatureAction> iter = mActions.iterator();
655        while (iter.hasNext()) {
656            FeatureAction action = iter.next();
657            action.finish(false);
658            iter.remove();
659        }
660    }
661
662    /**
663     * Send a key event to other device.
664     *
665     * @param keyCode key code defined in {@link android.view.KeyEvent}
666     * @param isPressed {@code true} for key down event
667     */
668    protected void sendKeyEvent(int keyCode, boolean isPressed) {
669        Slog.w(TAG, "sendKeyEvent not implemented");
670    }
671}
672