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