HdmiCecLocalDeviceTv.java revision 2b152015ff94f20b9ec3ef284fb83105f8b3c831
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.content.Intent;
20import android.hardware.hdmi.HdmiCecDeviceInfo;
21import android.hardware.hdmi.HdmiControlManager;
22import android.hardware.hdmi.HdmiRecordSources;
23import android.hardware.hdmi.IHdmiControlCallback;
24import android.media.AudioManager;
25import android.media.AudioSystem;
26import android.os.RemoteException;
27import android.os.SystemProperties;
28import android.os.UserHandle;
29import android.provider.Settings.Global;
30import android.util.Slog;
31import android.util.SparseArray;
32
33import com.android.internal.annotations.GuardedBy;
34import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
35import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
36
37import java.io.UnsupportedEncodingException;
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.Collections;
41import java.util.List;
42import java.util.Locale;
43
44/**
45 * Represent a logical device of type TV residing in Android system.
46 */
47final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
48    private static final String TAG = "HdmiCecLocalDeviceTv";
49
50    // Whether ARC is available or not. "true" means that ARC is estabilished between TV and
51    // AVR as audio receiver.
52    @ServiceThreadOnly
53    private boolean mArcEstablished = false;
54
55    // Whether ARC feature is enabled or not.
56    private boolean mArcFeatureEnabled = false;
57
58    // Whether System audio mode is activated or not.
59    // This becomes true only when all system audio sequences are finished.
60    @GuardedBy("mLock")
61    private boolean mSystemAudioActivated = false;
62
63    // The previous port id (input) before switching to the new one. This is remembered in order to
64    // be able to switch to it upon receiving <Inactive Source> from currently active source.
65    // This remains valid only when the active source was switched via one touch play operation
66    // (either by TV or source device). Manual port switching invalidates this value to
67    // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything.
68    @GuardedBy("mLock")
69    private int mPrevPortId;
70
71    @GuardedBy("mLock")
72    private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME;
73
74    @GuardedBy("mLock")
75    private boolean mSystemAudioMute = false;
76
77    // Copy of mDeviceInfos to guarantee thread-safety.
78    @GuardedBy("mLock")
79    private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
80    // All external cec input(source) devices. Does not include system audio device.
81    @GuardedBy("mLock")
82    private List<HdmiCecDeviceInfo> mSafeExternalInputs = Collections.emptyList();
83
84    // Map-like container of all cec devices including local ones.
85    // A logical address of device is used as key of container.
86    // This is not thread-safe. For external purpose use mSafeDeviceInfos.
87    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
88
89    // If true, TV going to standby mode puts other devices also to standby.
90    private boolean mAutoDeviceOff;
91
92    // If true, TV wakes itself up when receiving <Text/Image View On>.
93    private boolean mAutoWakeup;
94
95    HdmiCecLocalDeviceTv(HdmiControlService service) {
96        super(service, HdmiCecDeviceInfo.DEVICE_TV);
97        mPrevPortId = Constants.INVALID_PORT_ID;
98        mAutoDeviceOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
99                true);
100        mAutoWakeup = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, true);
101    }
102
103    @Override
104    @ServiceThreadOnly
105    protected void onAddressAllocated(int logicalAddress, boolean fromBootup) {
106        assertRunOnServiceThread();
107        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
108                mAddress, mService.getPhysicalAddress(), mDeviceType));
109        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
110                mAddress, mService.getVendorId()));
111        launchRoutingControl(fromBootup);
112        launchDeviceDiscovery();
113    }
114
115    @Override
116    @ServiceThreadOnly
117    protected int getPreferredAddress() {
118        assertRunOnServiceThread();
119        return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_TV,
120                Constants.ADDR_UNREGISTERED);
121    }
122
123    @Override
124    @ServiceThreadOnly
125    protected void setPreferredAddress(int addr) {
126        assertRunOnServiceThread();
127        SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_TV, String.valueOf(addr));
128    }
129
130    /**
131     * Performs the action 'device select', or 'one touch play' initiated by TV.
132     *
133     * @param targetAddress logical address of the device to select
134     * @param callback callback object to report the result with
135     */
136    @ServiceThreadOnly
137    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
138        assertRunOnServiceThread();
139        if (targetAddress == Constants.ADDR_INTERNAL) {
140            handleSelectInternalSource();
141            // Switching to internal source is always successful even when CEC control is disabled.
142            setActiveSource(targetAddress);
143            invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
144            return;
145        }
146        if (!mService.isControlEnabled()) {
147            setActiveSource(targetAddress);
148            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
149            return;
150        }
151        HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress);
152        if (targetDevice == null) {
153            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
154            return;
155        }
156        removeAction(DeviceSelectAction.class);
157        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
158    }
159
160    @ServiceThreadOnly
161    private void handleSelectInternalSource() {
162        assertRunOnServiceThread();
163        // Seq #18
164        if (mService.isControlEnabled() && getActiveSource() != mAddress) {
165            updateActiveSource(mAddress, mService.getPhysicalAddress());
166            // TODO: Check if this comes from <Text/Image View On> - if true, do nothing.
167            HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
168                    mAddress, mService.getPhysicalAddress());
169            mService.sendCecCommand(activeSource);
170        }
171    }
172
173    @ServiceThreadOnly
174    void updateActiveSource(int activeSource, int activePath) {
175        assertRunOnServiceThread();
176        // Seq #14
177        if (activeSource == getActiveSource() && activePath == getActivePath()) {
178            return;
179        }
180        setActiveSource(activeSource);
181        setActivePath(activePath);
182        if (getDeviceInfo(activeSource) != null && activeSource != mAddress) {
183            if (mService.pathToPortId(activePath) == getActivePortId()) {
184                setPrevPortId(getActivePortId());
185            }
186            // TODO: Show the OSD banner related to the new active source device.
187        } else {
188            // TODO: If displayed, remove the OSD banner related to the previous
189            //       active source device.
190        }
191    }
192
193    int getPortId(int physicalAddress) {
194        return mService.pathToPortId(physicalAddress);
195    }
196
197    /**
198     * Returns the previous port id kept to handle input switching on <Inactive Source>.
199     */
200    int getPrevPortId() {
201        synchronized (mLock) {
202            return mPrevPortId;
203        }
204    }
205
206    /**
207     * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be
208     * taken for <Inactive Source>.
209     */
210    void setPrevPortId(int portId) {
211        synchronized (mLock) {
212            mPrevPortId = portId;
213        }
214    }
215
216    @ServiceThreadOnly
217    void updateActivePortId(int portId) {
218        assertRunOnServiceThread();
219        // Seq #15
220        if (portId == getActivePortId()) {
221            return;
222        }
223        setPrevPortId(portId);
224        // TODO: Actually switch the physical port here. Handle PAP/PIP as well.
225        //       Show OSD port change banner
226        mService.invokeInputChangeListener(getActiveSource());
227    }
228
229    @ServiceThreadOnly
230    void doManualPortSwitching(int portId, IHdmiControlCallback callback) {
231        assertRunOnServiceThread();
232        // Seq #20
233        if (!mService.isValidPortId(portId)) {
234            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
235            return;
236        }
237        if (!mService.isControlEnabled()) {
238            setActivePortId(portId);
239            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
240            return;
241        }
242        if (portId == getActivePortId()) {
243            invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
244            return;
245        }
246        setActivePortId(portId);
247        // TODO: Return immediately if the operation is triggered by <Text/Image View On>
248        //       and this is the first notification about the active input after power-on
249        //       (switch to HDMI didn't happen so far but is expected to happen soon).
250        removeAction(RoutingControlAction.class);
251
252        int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId()));
253        int newPath = mService.portIdToPath(portId);
254        HdmiCecMessage routingChange =
255                HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
256        mService.sendCecCommand(routingChange);
257        addAndStartAction(new RoutingControlAction(this, newPath, false, callback));
258    }
259
260    int getPowerStatus() {
261        return mService.getPowerStatus();
262    }
263
264    /**
265     * Sends key to a target CEC device.
266     *
267     * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}.
268     * @param isPressed true if this is key press event
269     */
270    @Override
271    @ServiceThreadOnly
272    protected void sendKeyEvent(int keyCode, boolean isPressed) {
273        assertRunOnServiceThread();
274        List<SendKeyAction> action = getActions(SendKeyAction.class);
275        if (!action.isEmpty()) {
276            action.get(0).processKeyEvent(keyCode, isPressed);
277        } else {
278            if (isPressed) {
279                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
280            } else {
281                Slog.w(TAG, "Discard key release event");
282            }
283        }
284    }
285
286    private static void invokeCallback(IHdmiControlCallback callback, int result) {
287        if (callback == null) {
288            return;
289        }
290        try {
291            callback.onComplete(result);
292        } catch (RemoteException e) {
293            Slog.e(TAG, "Invoking callback failed:" + e);
294        }
295    }
296
297    @Override
298    @ServiceThreadOnly
299    protected boolean handleActiveSource(HdmiCecMessage message) {
300        assertRunOnServiceThread();
301        int address = message.getSource();
302        int path = HdmiUtils.twoBytesToInt(message.getParams());
303        if (getDeviceInfo(address) == null) {
304            handleNewDeviceAtTheTailOfActivePath(path);
305        } else {
306            ActiveSourceHandler.create(this, null).process(address, path);
307        }
308        return true;
309    }
310
311    @Override
312    @ServiceThreadOnly
313    protected boolean handleInactiveSource(HdmiCecMessage message) {
314        assertRunOnServiceThread();
315        // Seq #10
316
317        // Ignore <Inactive Source> from non-active source device.
318        if (getActiveSource() != message.getSource()) {
319            return true;
320        }
321        if (isProhibitMode()) {
322            return true;
323        }
324        int portId = getPrevPortId();
325        if (portId != Constants.INVALID_PORT_ID) {
326            // TODO: Do this only if TV is not showing multiview like PIP/PAP.
327
328            HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource());
329            if (inactiveSource == null) {
330                return true;
331            }
332            if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
333                return true;
334            }
335            // TODO: Switch the TV freeze mode off
336
337            setActivePortId(portId);
338            doManualPortSwitching(portId, null);
339            setPrevPortId(Constants.INVALID_PORT_ID);
340        }
341        return true;
342    }
343
344    @Override
345    @ServiceThreadOnly
346    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
347        assertRunOnServiceThread();
348        // Seq #19
349        if (mAddress == getActiveSource()) {
350            mService.sendCecCommand(
351                    HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath()));
352        }
353        return true;
354    }
355
356    @Override
357    @ServiceThreadOnly
358    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
359        assertRunOnServiceThread();
360        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
361                mAddress, Locale.getDefault().getISO3Language());
362        // TODO: figure out how to handle failed to get language code.
363        if (command != null) {
364            mService.sendCecCommand(command);
365        } else {
366            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
367        }
368        return true;
369    }
370
371    @Override
372    @ServiceThreadOnly
373    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
374        assertRunOnServiceThread();
375        // Ignore if [Device Discovery Action] is going on.
376        if (hasAction(DeviceDiscoveryAction.class)) {
377            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
378                    + "because Device Discovery Action is on-going:" + message);
379            return true;
380        }
381
382        int path = HdmiUtils.twoBytesToInt(message.getParams());
383        int address = message.getSource();
384        if (!isInDeviceList(path, address)) {
385            handleNewDeviceAtTheTailOfActivePath(path);
386        }
387        startNewDeviceAction(address, path);
388        return true;
389    }
390
391    void startNewDeviceAction(int address, int path) {
392        for (NewDeviceAction action : getActions(NewDeviceAction.class)) {
393            // If there is new device action which has the same logical address and path
394            // ignore new request.
395            // NewDeviceAction is created whenever it receives <Report Physical Address>.
396            // And there is a chance starting NewDeviceAction for the same source.
397            // Usually, new device sends <Report Physical Address> when it's plugged
398            // in. However, TV can detect a new device from HotPlugDetectionAction,
399            // which sends <Give Physical Address> to the source for newly detected
400            // device.
401            if (action.isActionOf(address, path)) {
402                return;
403            }
404        }
405
406        addAndStartAction(new NewDeviceAction(this, address, path));
407    }
408
409    private void handleNewDeviceAtTheTailOfActivePath(int path) {
410        // Seq #22
411        if (isTailOfActivePath(path, getActivePath())) {
412            removeAction(RoutingControlAction.class);
413            int newPath = mService.portIdToPath(getActivePortId());
414            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
415                    mAddress, getActivePath(), newPath));
416            addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null));
417        }
418    }
419
420    /**
421     * Whether the given path is located in the tail of current active path.
422     *
423     * @param path to be tested
424     * @param activePath current active path
425     * @return true if the given path is located in the tail of current active path; otherwise,
426     *         false
427     */
428    static boolean isTailOfActivePath(int path, int activePath) {
429        // If active routing path is internal source, return false.
430        if (activePath == 0) {
431            return false;
432        }
433        for (int i = 12; i >= 0; i -= 4) {
434            int curActivePath = (activePath >> i) & 0xF;
435            if (curActivePath == 0) {
436                return true;
437            } else {
438                int curPath = (path >> i) & 0xF;
439                if (curPath != curActivePath) {
440                    return false;
441                }
442            }
443        }
444        return false;
445    }
446
447    @Override
448    @ServiceThreadOnly
449    protected boolean handleRoutingChange(HdmiCecMessage message) {
450        assertRunOnServiceThread();
451        // Seq #21
452        byte[] params = message.getParams();
453        int currentPath = HdmiUtils.twoBytesToInt(params);
454        if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) {
455            int newPath = HdmiUtils.twoBytesToInt(params, 2);
456            setActivePath(newPath);
457            removeAction(RoutingControlAction.class);
458            addAndStartAction(new RoutingControlAction(this, newPath, true, null));
459        }
460        return true;
461    }
462
463    @Override
464    @ServiceThreadOnly
465    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
466        assertRunOnServiceThread();
467
468        byte params[] = message.getParams();
469        int mute = params[0] & 0x80;
470        int volume = params[0] & 0x7F;
471        setAudioStatus(mute == 0x80, volume);
472        return true;
473    }
474
475    @Override
476    @ServiceThreadOnly
477    protected boolean handleTextViewOn(HdmiCecMessage message) {
478        assertRunOnServiceThread();
479        if (mService.isPowerStandbyOrTransient() && mAutoWakeup) {
480            mService.wakeUp();
481        }
482        // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel
483        //       that represents the source device.
484        return true;
485    }
486
487    @Override
488    @ServiceThreadOnly
489    protected boolean handleImageViewOn(HdmiCecMessage message) {
490        assertRunOnServiceThread();
491        // Currently, it's the same as <Text View On>.
492        return handleTextViewOn(message);
493    }
494
495    @Override
496    @ServiceThreadOnly
497    protected boolean handleSetOsdName(HdmiCecMessage message) {
498        int source = message.getSource();
499        HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source);
500        // If the device is not in device list, ignore it.
501        if (deviceInfo == null) {
502            Slog.e(TAG, "No source device info for <Set Osd Name>." + message);
503            return true;
504        }
505        String osdName = null;
506        try {
507            osdName = new String(message.getParams(), "US-ASCII");
508        } catch (UnsupportedEncodingException e) {
509            Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
510            return true;
511        }
512
513        if (deviceInfo.getDisplayName().equals(osdName)) {
514            Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message);
515            return true;
516        }
517
518        addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(),
519                deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(),
520                deviceInfo.getDeviceType(), deviceInfo.getVendorId(), osdName));
521        return true;
522    }
523
524    @ServiceThreadOnly
525    private void launchDeviceDiscovery() {
526        assertRunOnServiceThread();
527        clearDeviceInfoList();
528        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
529                new DeviceDiscoveryCallback() {
530                    @Override
531                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
532                        for (HdmiCecDeviceInfo info : deviceInfos) {
533                            addCecDevice(info);
534                        }
535
536                        // Since we removed all devices when it's start and
537                        // device discovery action does not poll local devices,
538                        // we should put device info of local device manually here
539                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
540                            addCecDevice(device.getDeviceInfo());
541                        }
542
543                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
544
545                        // If there is AVR, initiate System Audio Auto initiation action,
546                        // which turns on and off system audio according to last system
547                        // audio setting.
548                        if (mSystemAudioActivated && getAvrDeviceInfo() != null) {
549                            addAndStartAction(new SystemAudioAutoInitiationAction(
550                                    HdmiCecLocalDeviceTv.this,
551                                    getAvrDeviceInfo().getLogicalAddress()));
552                            if (mArcEstablished) {
553                                startArcAction(true);
554                            }
555                        }
556                    }
557                });
558        addAndStartAction(action);
559    }
560
561    // Clear all device info.
562    @ServiceThreadOnly
563    private void clearDeviceInfoList() {
564        assertRunOnServiceThread();
565        for (HdmiCecDeviceInfo info : mSafeExternalInputs) {
566            mService.invokeDeviceEventListeners(info, false);
567        }
568        mDeviceInfos.clear();
569        updateSafeDeviceInfoList();
570    }
571
572    @ServiceThreadOnly
573    // Seq #32
574    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
575        assertRunOnServiceThread();
576        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
577            setSystemAudioMode(false, true);
578            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
579            return;
580        }
581        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
582        if (avr == null) {
583            setSystemAudioMode(false, true);
584            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
585            return;
586        }
587
588        addAndStartAction(
589                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
590    }
591
592    // # Seq 25
593    void setSystemAudioMode(boolean on, boolean updateSetting) {
594        if (updateSetting) {
595            mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
596        }
597        updateAudioManagerForSystemAudio(on);
598        synchronized (mLock) {
599            if (mSystemAudioActivated != on) {
600                mSystemAudioActivated = on;
601                mService.announceSystemAudioModeChange(on);
602            }
603        }
604    }
605
606    private void updateAudioManagerForSystemAudio(boolean on) {
607        // TODO: remove output device, once update AudioManager api.
608        mService.getAudioManager().setHdmiSystemAudioSupported(on);
609    }
610
611    boolean isSystemAudioActivated() {
612        if (getAvrDeviceInfo() == null) {
613            return false;
614        }
615        synchronized (mLock) {
616            return mSystemAudioActivated;
617        }
618    }
619
620    boolean getSystemAudioModeSetting() {
621        return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
622    }
623
624    /**
625     * Change ARC status into the given {@code enabled} status.
626     *
627     * @return {@code true} if ARC was in "Enabled" status
628     */
629    @ServiceThreadOnly
630    boolean setArcStatus(boolean enabled) {
631        assertRunOnServiceThread();
632        boolean oldStatus = mArcEstablished;
633        // 1. Enable/disable ARC circuit.
634        mService.setAudioReturnChannel(enabled);
635        // 2. Notify arc status to audio service.
636        notifyArcStatusToAudioService(enabled);
637        // 3. Update arc status;
638        mArcEstablished = enabled;
639        return oldStatus;
640    }
641
642    private void notifyArcStatusToAudioService(boolean enabled) {
643        // Note that we don't set any name to ARC.
644        mService.getAudioManager().setWiredDeviceConnectionState(
645                AudioSystem.DEVICE_OUT_HDMI_ARC,
646                enabled ? 1 : 0, "");
647    }
648
649    /**
650     * Returns whether ARC is enabled or not.
651     */
652    @ServiceThreadOnly
653    boolean isArcEstabilished() {
654        assertRunOnServiceThread();
655        return mArcFeatureEnabled && mArcEstablished;
656    }
657
658    @ServiceThreadOnly
659    void changeArcFeatureEnabled(boolean enabled) {
660        assertRunOnServiceThread();
661
662        if (mArcFeatureEnabled != enabled) {
663            if (enabled) {
664                if (!mArcEstablished) {
665                    startArcAction(true);
666                }
667            } else {
668                if (mArcEstablished) {
669                    startArcAction(false);
670                }
671            }
672            mArcFeatureEnabled = enabled;
673        }
674    }
675
676    @ServiceThreadOnly
677    boolean isArcFeatureEnabled() {
678        assertRunOnServiceThread();
679        return mArcFeatureEnabled;
680    }
681
682    @ServiceThreadOnly
683    private void startArcAction(boolean enabled) {
684        assertRunOnServiceThread();
685        HdmiCecDeviceInfo info = getAvrDeviceInfo();
686        if (info == null) {
687            return;
688        }
689        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
690            return;
691        }
692
693        // Terminate opposite action and start action if not exist.
694        if (enabled) {
695            removeAction(RequestArcTerminationAction.class);
696            if (!hasAction(RequestArcInitiationAction.class)) {
697                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
698            }
699        } else {
700            removeAction(RequestArcInitiationAction.class);
701            if (!hasAction(RequestArcTerminationAction.class)) {
702                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
703            }
704        }
705    }
706
707    void setAudioStatus(boolean mute, int volume) {
708        synchronized (mLock) {
709            mSystemAudioMute = mute;
710            mSystemAudioVolume = volume;
711            int maxVolume = mService.getAudioManager().getStreamMaxVolume(
712                    AudioManager.STREAM_MUSIC);
713            mService.setAudioStatus(mute,
714                    VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
715        }
716    }
717
718    @ServiceThreadOnly
719    void changeVolume(int curVolume, int delta, int maxVolume) {
720        assertRunOnServiceThread();
721        if (delta == 0 || !isSystemAudioActivated()) {
722            return;
723        }
724
725        int targetVolume = curVolume + delta;
726        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
727        synchronized (mLock) {
728            // If new volume is the same as current system audio volume, just ignore it.
729            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
730            if (cecVolume == mSystemAudioVolume) {
731                // Update tv volume with system volume value.
732                mService.setAudioStatus(false,
733                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
734                return;
735            }
736        }
737
738        // Remove existing volume action.
739        removeAction(VolumeControlAction.class);
740
741        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
742        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
743                cecVolume, delta > 0));
744    }
745
746    @ServiceThreadOnly
747    void changeMute(boolean mute) {
748        assertRunOnServiceThread();
749        if (!isSystemAudioActivated()) {
750            return;
751        }
752
753        // Remove existing volume action.
754        removeAction(VolumeControlAction.class);
755        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
756        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
757    }
758
759    private boolean isSystemAudioOn() {
760
761
762        synchronized (mLock) {
763            return mSystemAudioActivated;
764        }
765    }
766
767    @Override
768    @ServiceThreadOnly
769    protected boolean handleInitiateArc(HdmiCecMessage message) {
770        assertRunOnServiceThread();
771        // In case where <Initiate Arc> is started by <Request ARC Initiation>
772        // need to clean up RequestArcInitiationAction.
773        removeAction(RequestArcInitiationAction.class);
774        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
775                message.getSource(), true);
776        addAndStartAction(action);
777        return true;
778    }
779
780    @Override
781    @ServiceThreadOnly
782    protected boolean handleTerminateArc(HdmiCecMessage message) {
783        assertRunOnServiceThread();
784        // In case where <Terminate Arc> is started by <Request ARC Termination>
785        // need to clean up RequestArcInitiationAction.
786        removeAction(RequestArcTerminationAction.class);
787        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
788                message.getSource(), false);
789        addAndStartAction(action);
790        return true;
791    }
792
793    @Override
794    @ServiceThreadOnly
795    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
796        assertRunOnServiceThread();
797        if (!isMessageForSystemAudio(message)) {
798            return false;
799        }
800        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
801                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
802        addAndStartAction(action);
803        return true;
804    }
805
806    @Override
807    @ServiceThreadOnly
808    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
809        assertRunOnServiceThread();
810        if (!isMessageForSystemAudio(message)) {
811            return false;
812        }
813        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
814        return true;
815    }
816
817    // Seq #53
818    @Override
819    @ServiceThreadOnly
820    protected boolean handleRecordTvScreen(HdmiCecMessage message) {
821        List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
822        if (!actions.isEmpty()) {
823            // Assumes only one OneTouchRecordAction.
824            OneTouchRecordAction action = actions.get(0);
825            if (action.getRecorderAddress() != message.getSource()) {
826                displayOsd(HdmiControlManager.MESSAGE_NO_RECORDING_PREVIOUS_RECORDING_IN_PROGRESS);
827            }
828            return super.handleRecordTvScreen(message);
829        }
830
831        int recorderAddress = message.getSource();
832        byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
833        startOneTouchRecord(recorderAddress, recordSource);
834        return true;
835    }
836
837    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
838        if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM
839                || message.getDestination() != Constants.ADDR_TV
840                || getAvrDeviceInfo() == null) {
841            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
842            return false;
843        }
844        return true;
845    }
846
847    /**
848     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
849     * logical address as new device info's.
850     *
851     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
852     *
853     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
854     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
855     *         that has the same logical address as new one has.
856     */
857    @ServiceThreadOnly
858    private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
859        assertRunOnServiceThread();
860        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
861        if (oldDeviceInfo != null) {
862            removeDeviceInfo(deviceInfo.getLogicalAddress());
863        }
864        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
865        updateSafeDeviceInfoList();
866        return oldDeviceInfo;
867    }
868
869    /**
870     * Remove a device info corresponding to the given {@code logicalAddress}.
871     * It returns removed {@link HdmiCecDeviceInfo} if exists.
872     *
873     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
874     *
875     * @param logicalAddress logical address of device to be removed
876     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
877     */
878    @ServiceThreadOnly
879    private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
880        assertRunOnServiceThread();
881        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
882        if (deviceInfo != null) {
883            mDeviceInfos.remove(logicalAddress);
884        }
885        updateSafeDeviceInfoList();
886        return deviceInfo;
887    }
888
889    /**
890     * Return a list of all {@link HdmiCecDeviceInfo}.
891     *
892     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
893     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
894     */
895    @ServiceThreadOnly
896    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
897        assertRunOnServiceThread();
898        if (includelLocalDevice) {
899            return HdmiUtils.sparseArrayToList(mDeviceInfos);
900        } else {
901            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
902            for (int i = 0; i < mDeviceInfos.size(); ++i) {
903                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
904                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
905                    infoList.add(info);
906                }
907            }
908            return infoList;
909        }
910    }
911
912    /**
913     * Return external input devices.
914     */
915    List<HdmiCecDeviceInfo> getSafeExternalInputs() {
916        synchronized (mLock) {
917            return mSafeExternalInputs;
918        }
919    }
920
921    @ServiceThreadOnly
922    private void updateSafeDeviceInfoList() {
923        assertRunOnServiceThread();
924        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
925        List<HdmiCecDeviceInfo> externalInputs = getInputDevices();
926        synchronized (mLock) {
927            mSafeAllDeviceInfos = copiedDevices;
928            mSafeExternalInputs = externalInputs;
929        }
930    }
931
932    /**
933     * Return a list of external cec input (source) devices.
934     *
935     * <p>Note that this effectively excludes non-source devices like system audio,
936     * secondary TV.
937     */
938    private List<HdmiCecDeviceInfo> getInputDevices() {
939        ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
940        for (int i = 0; i < mDeviceInfos.size(); ++i) {
941            HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
942            if (isLocalDeviceAddress(i)) {
943                continue;
944            }
945            if (info.isSourceType()) {
946                infoList.add(info);
947            }
948        }
949        return infoList;
950    }
951
952    @ServiceThreadOnly
953    private boolean isLocalDeviceAddress(int address) {
954        assertRunOnServiceThread();
955        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
956            if (device.isAddressOf(address)) {
957                return true;
958            }
959        }
960        return false;
961    }
962
963    @ServiceThreadOnly
964    HdmiCecDeviceInfo getAvrDeviceInfo() {
965        assertRunOnServiceThread();
966        return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
967    }
968
969    /**
970     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
971     *
972     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
973     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
974     *
975     * @param logicalAddress logical address to be retrieved
976     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
977     *         Returns null if no logical address matched
978     */
979    @ServiceThreadOnly
980    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
981        assertRunOnServiceThread();
982        return mDeviceInfos.get(logicalAddress);
983    }
984
985    boolean hasSystemAudioDevice() {
986        return getSafeAvrDeviceInfo() != null;
987    }
988
989    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
990        return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
991    }
992
993    /**
994     * Thread safe version of {@link #getDeviceInfo(int)}.
995     *
996     * @param logicalAddress logical address to be retrieved
997     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
998     *         Returns null if no logical address matched
999     */
1000    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
1001        synchronized (mLock) {
1002            return mSafeAllDeviceInfos.get(logicalAddress);
1003        }
1004    }
1005
1006    /**
1007     * Called when a device is newly added or a new device is detected or
1008     * existing device is updated.
1009     *
1010     * @param info device info of a new device.
1011     */
1012    @ServiceThreadOnly
1013    final void addCecDevice(HdmiCecDeviceInfo info) {
1014        assertRunOnServiceThread();
1015        addDeviceInfo(info);
1016        if (info.getLogicalAddress() == mAddress) {
1017            // The addition of TV device itself should not be notified.
1018            return;
1019        }
1020        mService.invokeDeviceEventListeners(info, true);
1021    }
1022
1023    /**
1024     * Called when a device is removed or removal of device is detected.
1025     *
1026     * @param address a logical address of a device to be removed
1027     */
1028    @ServiceThreadOnly
1029    final void removeCecDevice(int address) {
1030        assertRunOnServiceThread();
1031        HdmiCecDeviceInfo info = removeDeviceInfo(address);
1032
1033        mCecMessageCache.flushMessagesFrom(address);
1034        mService.invokeDeviceEventListeners(info, false);
1035    }
1036
1037    @ServiceThreadOnly
1038    void handleRemoveActiveRoutingPath(int path) {
1039        assertRunOnServiceThread();
1040        // Seq #23
1041        if (isTailOfActivePath(path, getActivePath())) {
1042            removeAction(RoutingControlAction.class);
1043            int newPath = mService.portIdToPath(getActivePortId());
1044            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1045                    mAddress, getActivePath(), newPath));
1046            addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null));
1047        }
1048    }
1049
1050    /**
1051     * Launch routing control process.
1052     *
1053     * @param routingForBootup true if routing control is initiated due to One Touch Play
1054     *        or TV power on
1055     */
1056    @ServiceThreadOnly
1057    void launchRoutingControl(boolean routingForBootup) {
1058        assertRunOnServiceThread();
1059        // Seq #24
1060        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1061            if (!routingForBootup && !isProhibitMode()) {
1062                removeAction(RoutingControlAction.class);
1063                int newPath = mService.portIdToPath(getActivePortId());
1064                setActivePath(newPath);
1065                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1066                        getActivePath(), newPath));
1067                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1068                        routingForBootup, null));
1069            }
1070        } else {
1071            int activePath = mService.getPhysicalAddress();
1072            setActivePath(activePath);
1073            if (!routingForBootup) {
1074                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1075                        activePath));
1076            }
1077        }
1078    }
1079
1080    /**
1081     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
1082     * the given routing path. CEC devices use routing path for its physical address to
1083     * describe the hierarchy of the devices in the network.
1084     *
1085     * @param path routing path or physical address
1086     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
1087     */
1088    @ServiceThreadOnly
1089    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
1090        assertRunOnServiceThread();
1091        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
1092            if (info.getPhysicalAddress() == path) {
1093                return info;
1094            }
1095        }
1096        return null;
1097    }
1098
1099    /**
1100     * Whether a device of the specified physical address and logical address exists
1101     * in a device info list. However, both are minimal condition and it could
1102     * be different device from the original one.
1103     *
1104     * @param logicalAddress logical address of a device to be searched
1105     * @param physicalAddress physical address of a device to be searched
1106     * @return true if exist; otherwise false
1107     */
1108    @ServiceThreadOnly
1109    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1110        assertRunOnServiceThread();
1111        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
1112        if (device == null) {
1113            return false;
1114        }
1115        return device.getPhysicalAddress() == physicalAddress;
1116    }
1117
1118    @Override
1119    @ServiceThreadOnly
1120    void onHotplug(int portId, boolean connected) {
1121        assertRunOnServiceThread();
1122
1123        // Tv device will have permanent HotplugDetectionAction.
1124        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1125        if (!hotplugActions.isEmpty()) {
1126            // Note that hotplug action is single action running on a machine.
1127            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1128            // It covers seq #40, #43.
1129            hotplugActions.get(0).pollAllDevicesNow();
1130        }
1131    }
1132
1133    @ServiceThreadOnly
1134    void setAutoDeviceOff(boolean enabled) {
1135        assertRunOnServiceThread();
1136        mAutoDeviceOff = enabled;
1137        mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, enabled);
1138    }
1139
1140    @ServiceThreadOnly
1141    void setAutoWakeup(boolean enabled) {
1142        assertRunOnServiceThread();
1143        mAutoWakeup = enabled;
1144        mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, enabled);
1145    }
1146
1147    @Override
1148    @ServiceThreadOnly
1149    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1150        super.disableDevice(initiatedByCec, callback);
1151        assertRunOnServiceThread();
1152        // Remove any repeated working actions.
1153        // HotplugDetectionAction will be reinstated during the wake up process.
1154        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1155        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1156        removeAction(DeviceDiscoveryAction.class);
1157        removeAction(HotplugDetectionAction.class);
1158        // Remove one touch record action.
1159        removeAction(OneTouchRecordAction.class);
1160
1161        disableSystemAudioIfExist();
1162        disableArcIfExist();
1163        clearDeviceInfoList();
1164        checkIfPendingActionsCleared();
1165    }
1166
1167    @ServiceThreadOnly
1168    private void disableSystemAudioIfExist() {
1169        assertRunOnServiceThread();
1170        if (getAvrDeviceInfo() == null) {
1171            return;
1172        }
1173
1174        // Seq #31.
1175        removeAction(SystemAudioActionFromAvr.class);
1176        removeAction(SystemAudioActionFromTv.class);
1177        removeAction(SystemAudioAutoInitiationAction.class);
1178        removeAction(SystemAudioStatusAction.class);
1179        removeAction(VolumeControlAction.class);
1180
1181        // Turn off the mode but do not write it the settings, so that the next time TV powers on
1182        // the system audio mode setting can be restored automatically.
1183        setSystemAudioMode(false, false);
1184    }
1185
1186    @ServiceThreadOnly
1187    private void disableArcIfExist() {
1188        assertRunOnServiceThread();
1189        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
1190        if (avr == null) {
1191            return;
1192        }
1193
1194        // Seq #44.
1195        removeAction(RequestArcInitiationAction.class);
1196        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1197            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1198        }
1199    }
1200
1201    @Override
1202    @ServiceThreadOnly
1203    protected void onStandby(boolean initiatedByCec) {
1204        assertRunOnServiceThread();
1205        // Seq #11
1206        if (!mService.isControlEnabled()) {
1207            return;
1208        }
1209        if (!initiatedByCec && mAutoDeviceOff) {
1210            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1211                    mAddress, Constants.ADDR_BROADCAST));
1212        }
1213    }
1214
1215    @Override
1216    @ServiceThreadOnly
1217    protected boolean handleStandby(HdmiCecMessage message) {
1218        assertRunOnServiceThread();
1219        // Seq #12
1220        // Tv accepts directly addressed <Standby> only.
1221        if (message.getDestination() == mAddress) {
1222            super.handleStandby(message);
1223        }
1224        return false;
1225    }
1226
1227    boolean isProhibitMode() {
1228        return mService.isProhibitMode();
1229    }
1230
1231    boolean isPowerStandbyOrTransient() {
1232        return mService.isPowerStandbyOrTransient();
1233    }
1234
1235    void displayOsd(int messageId) {
1236        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
1237        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
1238        mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
1239                HdmiControlService.PERMISSION);
1240    }
1241
1242    // Seq #54 and #55
1243    @ServiceThreadOnly
1244    void startOneTouchRecord(int recorderAddress, byte[] recordSource) {
1245        assertRunOnServiceThread();
1246        if (!mService.isControlEnabled()) {
1247            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1248            return;
1249        }
1250
1251        if (!checkRecorder(recorderAddress)) {
1252            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1253            return;
1254        }
1255
1256        if (!checkRecordSource(recordSource)) {
1257            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1258            return;
1259        }
1260
1261        addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource));
1262        Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:"
1263                + Arrays.toString(recordSource));
1264    }
1265
1266    @ServiceThreadOnly
1267    void stopOneTouchRecord(int recorderAddress) {
1268        assertRunOnServiceThread();
1269        if (!mService.isControlEnabled()) {
1270            Slog.w(TAG, "Can not stop one touch record. CEC control is disabled.");
1271            return;
1272        }
1273
1274        if (!checkRecorder(recorderAddress)) {
1275            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1276            return;
1277        }
1278
1279        // Remove one touch record action so that other one touch record can be started.
1280        removeAction(OneTouchRecordAction.class);
1281        mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress));
1282        Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress);
1283    }
1284
1285    private boolean checkRecorder(int recorderAddress) {
1286        HdmiCecDeviceInfo device = getDeviceInfo(recorderAddress);
1287        return (device != null)
1288                && (HdmiUtils.getTypeFromAddress(recorderAddress)
1289                        == HdmiCecDeviceInfo.DEVICE_RECORDER);
1290    }
1291
1292    private boolean checkRecordSource(byte[] recordSource) {
1293        return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource);
1294    }
1295
1296    @ServiceThreadOnly
1297    void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1298        assertRunOnServiceThread();
1299
1300        // TODO: implement this.
1301    }
1302
1303    @ServiceThreadOnly
1304    void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1305        assertRunOnServiceThread();
1306
1307        // TODO: implement this.
1308    }
1309}
1310