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