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