HdmiCecLocalDeviceTv.java revision 4d43d93743222311c6377d4904c19ccb93699d3b
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.HdmiCec;
20import android.hardware.hdmi.HdmiCecDeviceInfo;
21import android.hardware.hdmi.HdmiCecMessage;
22import android.hardware.hdmi.IHdmiControlCallback;
23import android.media.AudioSystem;
24import android.os.RemoteException;
25import android.util.Slog;
26import android.util.SparseArray;
27
28import com.android.internal.annotations.GuardedBy;
29import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
30import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
31
32import java.util.ArrayList;
33import java.util.Collections;
34import java.util.List;
35import java.util.Locale;
36
37/**
38 * Represent a logical device of type TV residing in Android system.
39 */
40final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
41    private static final String TAG = "HdmiCecLocalDeviceTv";
42
43    // Whether ARC is available or not. "true" means that ARC is estabilished between TV and
44    // AVR as audio receiver.
45    @ServiceThreadOnly
46    private boolean mArcEstablished = false;
47
48    // Whether ARC feature is enabled or not.
49    private boolean mArcFeatureEnabled = false;
50
51    // Whether SystemAudioMode is "On" or not.
52    @GuardedBy("mLock")
53    private boolean mSystemAudioMode;
54
55    // The previous port id (input) before switching to the new one. This is remembered in order to
56    // be able to switch to it upon receiving <Inactive Source> from currently active source.
57    // This remains valid only when the active source was switched via one touch play operation
58    // (either by TV or source device). Manual port switching invalidates this value to
59    // HdmiConstants.PORT_INVALID, for which case <Inactive Source> does not do anything.
60    @GuardedBy("mLock")
61    private int mPrevPortId;
62
63    @GuardedBy("mLock")
64    private int mSystemAudioVolume = HdmiConstants.UNKNOWN_VOLUME;
65
66    @GuardedBy("mLock")
67    private boolean mSystemAudioMute = false;
68
69    // Copy of mDeviceInfos to guarantee thread-safety.
70    @GuardedBy("mLock")
71    private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
72    // All external cec device which excludes local devices.
73    @GuardedBy("mLock")
74    private List<HdmiCecDeviceInfo> mSafeExternalDeviceInfos = Collections.emptyList();
75
76    // Map-like container of all cec devices including local ones.
77    // A logical address of device is used as key of container.
78    // This is not thread-safe. For external purpose use mSafeDeviceInfos.
79    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
80
81    // If true, TV going to standby mode puts other devices also to standby.
82    private boolean mAutoDeviceOff;
83
84    HdmiCecLocalDeviceTv(HdmiControlService service) {
85        super(service, HdmiCec.DEVICE_TV);
86        mPrevPortId = HdmiConstants.INVALID_PORT_ID;
87        // TODO: load system audio mode and set it to mSystemAudioMode.
88    }
89
90    @Override
91    @ServiceThreadOnly
92    protected void onAddressAllocated(int logicalAddress) {
93        assertRunOnServiceThread();
94        // TODO: vendor-specific initialization here.
95
96        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
97                mAddress, mService.getPhysicalAddress(), mDeviceType));
98        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
99                mAddress, mService.getVendorId()));
100
101        launchDeviceDiscovery();
102        // TODO: Start routing control action
103    }
104
105    /**
106     * Performs the action 'device select', or 'one touch play' initiated by TV.
107     *
108     * @param targetAddress logical address of the device to select
109     * @param callback callback object to report the result with
110     */
111    @ServiceThreadOnly
112    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
113        assertRunOnServiceThread();
114        if (targetAddress == HdmiCec.ADDR_INTERNAL) {
115            handleSelectInternalSource(callback);
116            return;
117        }
118        HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress);
119        if (targetDevice == null) {
120            invokeCallback(callback, HdmiCec.RESULT_TARGET_NOT_AVAILABLE);
121            return;
122        }
123        removeAction(DeviceSelectAction.class);
124        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
125    }
126
127    @ServiceThreadOnly
128    private void handleSelectInternalSource(IHdmiControlCallback callback) {
129        assertRunOnServiceThread();
130        // Seq #18
131        if (mService.isControlEnabled() && getActiveSource() != mAddress) {
132            updateActiveSource(mAddress, mService.getPhysicalAddress());
133            // TODO: Check if this comes from <Text/Image View On> - if true, do nothing.
134            HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
135                    mAddress, mService.getPhysicalAddress());
136            mService.sendCecCommand(activeSource);
137        }
138    }
139
140    @ServiceThreadOnly
141    void updateActiveSource(int activeSource, int activePath) {
142        assertRunOnServiceThread();
143        // Seq #14
144        if (activeSource == getActiveSource() && activePath == getActivePath()) {
145            return;
146        }
147        setActiveSource(activeSource);
148        setActivePath(activePath);
149        if (getDeviceInfo(activeSource) != null && activeSource != mAddress) {
150            if (mService.pathToPortId(activePath) == getActivePortId()) {
151                setPrevPortId(getActivePortId());
152            }
153            // TODO: Show the OSD banner related to the new active source device.
154        } else {
155            // TODO: If displayed, remove the OSD banner related to the previous
156            //       active source device.
157        }
158    }
159
160    /**
161     * Returns the previous port id kept to handle input switching on <Inactive Source>.
162     */
163    int getPrevPortId() {
164        synchronized (mLock) {
165            return mPrevPortId;
166        }
167    }
168
169    /**
170     * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be
171     * taken for <Inactive Source>.
172     */
173    void setPrevPortId(int portId) {
174        synchronized (mLock) {
175            mPrevPortId = portId;
176        }
177    }
178
179    @ServiceThreadOnly
180    void updateActivePortId(int portId) {
181        assertRunOnServiceThread();
182        // Seq #15
183        if (portId == getActivePortId()) {
184            return;
185        }
186        setPrevPortId(portId);
187        // TODO: Actually switch the physical port here. Handle PAP/PIP as well.
188        //       Show OSD port change banner
189    }
190
191    @ServiceThreadOnly
192    void doManualPortSwitching(int portId, IHdmiControlCallback callback) {
193        assertRunOnServiceThread();
194        // Seq #20
195        if (!mService.isControlEnabled() || portId == getActivePortId()) {
196            invokeCallback(callback, HdmiCec.RESULT_INCORRECT_MODE);
197            return;
198        }
199        // TODO: Make sure this call does not stem from <Active Source> message reception.
200
201        setActivePortId(portId);
202        // TODO: Return immediately if the operation is triggered by <Text/Image View On>
203        //       and this is the first notification about the active input after power-on.
204        // TODO: Handle invalid port id / active input which should be treated as an
205        //       internal tuner.
206
207        removeAction(RoutingControlAction.class);
208
209        int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId()));
210        int newPath = mService.portIdToPath(portId);
211        HdmiCecMessage routingChange =
212                HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
213        mService.sendCecCommand(routingChange);
214        addAndStartAction(new RoutingControlAction(this, newPath, callback));
215    }
216
217    /**
218     * Sends key to a target CEC device.
219     *
220     * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}.
221     * @param isPressed true if this is keypress event
222     */
223    @ServiceThreadOnly
224    void sendKeyEvent(int keyCode, boolean isPressed) {
225        assertRunOnServiceThread();
226        List<SendKeyAction> action = getActions(SendKeyAction.class);
227        if (!action.isEmpty()) {
228            action.get(0).processKeyEvent(keyCode, isPressed);
229        } else {
230            if (isPressed) {
231                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
232            } else {
233                Slog.w(TAG, "Discard key release event");
234            }
235        }
236    }
237
238    private static void invokeCallback(IHdmiControlCallback callback, int result) {
239        if (callback == null) {
240            return;
241        }
242        try {
243            callback.onComplete(result);
244        } catch (RemoteException e) {
245            Slog.e(TAG, "Invoking callback failed:" + e);
246        }
247    }
248
249    @Override
250    @ServiceThreadOnly
251    protected boolean handleActiveSource(HdmiCecMessage message) {
252        assertRunOnServiceThread();
253        int address = message.getSource();
254        int path = HdmiUtils.twoBytesToInt(message.getParams());
255        if (getDeviceInfo(address) == null) {
256            handleNewDeviceAtTheTailOfActivePath(address, path);
257        } else {
258            ActiveSourceHandler.create(this, null).process(address, path);
259        }
260        return true;
261    }
262
263    @Override
264    @ServiceThreadOnly
265    protected boolean handleInactiveSource(HdmiCecMessage message) {
266        assertRunOnServiceThread();
267        // Seq #10
268
269        // Ignore <Inactive Source> from non-active source device.
270        if (getActiveSource() != message.getSource()) {
271            return true;
272        }
273        if (isProhibitMode()) {
274            return true;
275        }
276        int portId = getPrevPortId();
277        if (portId != HdmiConstants.INVALID_PORT_ID) {
278            // TODO: Do this only if TV is not showing multiview like PIP/PAP.
279
280            HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource());
281            if (inactiveSource == null) {
282                return true;
283            }
284            if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
285                return true;
286            }
287            // TODO: Switch the TV freeze mode off
288
289            setActivePortId(portId);
290            doManualPortSwitching(portId, null);
291            setPrevPortId(HdmiConstants.INVALID_PORT_ID);
292        }
293        return true;
294    }
295
296    @Override
297    @ServiceThreadOnly
298    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
299        assertRunOnServiceThread();
300        // Seq #19
301        if (mAddress == getActiveSource()) {
302            mService.sendCecCommand(
303                    HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath()));
304        }
305        return true;
306    }
307
308    @Override
309    @ServiceThreadOnly
310    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
311        assertRunOnServiceThread();
312        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
313                mAddress, Locale.getDefault().getISO3Language());
314        // TODO: figure out how to handle failed to get language code.
315        if (command != null) {
316            mService.sendCecCommand(command);
317        } else {
318            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
319        }
320        return true;
321    }
322
323    @Override
324    @ServiceThreadOnly
325    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
326        assertRunOnServiceThread();
327        // Ignore if [Device Discovery Action] is going on.
328        if (hasAction(DeviceDiscoveryAction.class)) {
329            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
330                    + "because Device Discovery Action is on-going:" + message);
331            return true;
332        }
333
334        int path = HdmiUtils.twoBytesToInt(message.getParams());
335        int address = message.getSource();
336        if (!isInDeviceList(path, address)) {
337            handleNewDeviceAtTheTailOfActivePath(address, path);
338        }
339        addAndStartAction(new NewDeviceAction(this, address, path));
340        return true;
341    }
342
343    private void handleNewDeviceAtTheTailOfActivePath(int address, int path) {
344        // Seq #22
345        if (isTailOfActivePath(path, getActivePath())) {
346            removeAction(RoutingControlAction.class);
347            int newPath = mService.portIdToPath(getActivePortId());
348            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
349                    mAddress, getActivePath(), newPath));
350            addAndStartAction(new RoutingControlAction(this, getActivePortId(), null));
351        }
352    }
353
354    /**
355     * Whether the given path is located in the tail of current active path.
356     *
357     * @param path to be tested
358     * @param activePath current active path
359     * @return true if the given path is located in the tail of current active path; otherwise,
360     *         false
361     */
362    static boolean isTailOfActivePath(int path, int activePath) {
363        // If active routing path is internal source, return false.
364        if (activePath == 0) {
365            return false;
366        }
367        for (int i = 12; i >= 0; i -= 4) {
368            int curActivePath = (activePath >> i) & 0xF;
369            if (curActivePath == 0) {
370                return true;
371            } else {
372                int curPath = (path >> i) & 0xF;
373                if (curPath != curActivePath) {
374                    return false;
375                }
376            }
377        }
378        return false;
379    }
380
381    @Override
382    @ServiceThreadOnly
383    protected boolean handleRoutingChange(HdmiCecMessage message) {
384        assertRunOnServiceThread();
385        // Seq #21
386        byte[] params = message.getParams();
387        if (params.length != 4) {
388            Slog.w(TAG, "Wrong parameter: " + message);
389            return true;
390        }
391        int currentPath = HdmiUtils.twoBytesToInt(params);
392        if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) {
393            int newPath = HdmiUtils.twoBytesToInt(params, 2);
394            setActivePath(newPath);
395            removeAction(RoutingControlAction.class);
396            addAndStartAction(new RoutingControlAction(this, newPath, null));
397        }
398        return true;
399    }
400
401    @Override
402    @ServiceThreadOnly
403    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
404        assertRunOnServiceThread();
405        List<VendorSpecificAction> actions = Collections.emptyList();
406        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
407
408        // We assume that there can be multiple vendor-specific command actions running
409        // at the same time. Pass the message to each action to see if one of them needs it.
410        for (VendorSpecificAction action : actions) {
411            if (action.processCommand(message)) {
412                return true;
413            }
414        }
415        // Handle the message here if it is not already consumed by one of the running actions.
416        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
417        // vendor-specific action:
418        //
419        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
420        //
421        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
422        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
423                message.getDestination(), message.getSource(), message.getOpcode(),
424                HdmiConstants.ABORT_REFUSED));
425        return true;
426    }
427
428    @Override
429    @ServiceThreadOnly
430    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
431        assertRunOnServiceThread();
432
433        byte params[] = message.getParams();
434        if (params.length < 1) {
435            Slog.w(TAG, "Invalide <Report Audio Status> message:" + message);
436            return true;
437        }
438        int mute = params[0] & 0x80;
439        int volume = params[0] & 0x7F;
440        setAudioStatus(mute == 0x80, volume);
441        return true;
442    }
443
444    @Override
445    @ServiceThreadOnly
446    protected boolean handleTextViewOn(HdmiCecMessage message) {
447        assertRunOnServiceThread();
448        if (mService.isPowerStandbyOrTransient()) {
449            mService.wakeUp();
450        }
451        // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel
452        //       that represents the source device.
453        return true;
454    }
455
456    @Override
457    @ServiceThreadOnly
458    protected boolean handleImageViewOn(HdmiCecMessage message) {
459        assertRunOnServiceThread();
460        // Currently, it's the same as <Text View On>.
461        return handleTextViewOn(message);
462    }
463
464    @ServiceThreadOnly
465    private void launchDeviceDiscovery() {
466        assertRunOnServiceThread();
467        clearDeviceInfoList();
468        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
469                new DeviceDiscoveryCallback() {
470                    @Override
471                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
472                        for (HdmiCecDeviceInfo info : deviceInfos) {
473                            addCecDevice(info);
474                        }
475
476                        // Since we removed all devices when it's start and
477                        // device discovery action does not poll local devices,
478                        // we should put device info of local device manually here
479                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
480                            addCecDevice(device.getDeviceInfo());
481                        }
482
483                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
484
485                        // If there is AVR, initiate System Audio Auto initiation action,
486                        // which turns on and off system audio according to last system
487                        // audio setting.
488                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
489                        if (avrInfo != null) {
490                            addAndStartAction(new SystemAudioAutoInitiationAction(
491                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
492                            if (mArcEstablished) {
493                                startArcAction(true);
494                            }
495                        }
496                    }
497                });
498        addAndStartAction(action);
499    }
500
501    // Clear all device info.
502    @ServiceThreadOnly
503    private void clearDeviceInfoList() {
504        assertRunOnServiceThread();
505        mDeviceInfos.clear();
506        updateSafeDeviceInfoList();
507    }
508
509    @ServiceThreadOnly
510    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
511        assertRunOnServiceThread();
512        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
513        if (avr == null) {
514            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
515            return;
516        }
517
518        addAndStartAction(
519                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
520    }
521
522    // # Seq 25
523    void setSystemAudioMode(boolean on) {
524        synchronized (mLock) {
525            if (on != mSystemAudioMode) {
526                mSystemAudioMode = on;
527                // TODO: Need to set the preference for SystemAudioMode.
528                mService.announceSystemAudioModeChange(on);
529            }
530        }
531    }
532
533    boolean getSystemAudioMode() {
534        synchronized (mLock) {
535            return mSystemAudioMode;
536        }
537    }
538
539    /**
540     * Change ARC status into the given {@code enabled} status.
541     *
542     * @return {@code true} if ARC was in "Enabled" status
543     */
544    @ServiceThreadOnly
545    boolean setArcStatus(boolean enabled) {
546        assertRunOnServiceThread();
547        boolean oldStatus = mArcEstablished;
548        // 1. Enable/disable ARC circuit.
549        mService.setAudioReturnChannel(enabled);
550        // 2. Notify arc status to audio service.
551        notifyArcStatusToAudioService(enabled);
552        // 3. Update arc status;
553        mArcEstablished = enabled;
554        return oldStatus;
555    }
556
557    private void notifyArcStatusToAudioService(boolean enabled) {
558        // Note that we don't set any name to ARC.
559        mService.getAudioManager().setWiredDeviceConnectionState(
560                AudioSystem.DEVICE_OUT_HDMI_ARC,
561                enabled ? 1 : 0, "");
562    }
563
564    /**
565     * Returns whether ARC is enabled or not.
566     */
567    @ServiceThreadOnly
568    boolean isArcEstabilished() {
569        assertRunOnServiceThread();
570        return mArcFeatureEnabled && mArcEstablished;
571    }
572
573    @ServiceThreadOnly
574    void changeArcFeatureEnabled(boolean enabled) {
575        assertRunOnServiceThread();
576
577        if (mArcFeatureEnabled != enabled) {
578            if (enabled) {
579                if (!mArcEstablished) {
580                    startArcAction(true);
581                }
582            } else {
583                if (mArcEstablished) {
584                    startArcAction(false);
585                }
586            }
587            mArcFeatureEnabled = enabled;
588        }
589    }
590
591    @ServiceThreadOnly
592    boolean isArcFeatureEnabled() {
593        assertRunOnServiceThread();
594        return mArcFeatureEnabled;
595    }
596
597    @ServiceThreadOnly
598    private void startArcAction(boolean enabled) {
599        assertRunOnServiceThread();
600        HdmiCecDeviceInfo info = getAvrDeviceInfo();
601        if (info == null) {
602            return;
603        }
604        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
605            return;
606        }
607
608        // Terminate opposite action and start action if not exist.
609        if (enabled) {
610            removeAction(RequestArcTerminationAction.class);
611            if (!hasAction(RequestArcInitiationAction.class)) {
612                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
613            }
614        } else {
615            removeAction(RequestArcInitiationAction.class);
616            if (!hasAction(RequestArcTerminationAction.class)) {
617                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
618            }
619        }
620    }
621
622    void setAudioStatus(boolean mute, int volume) {
623        synchronized (mLock) {
624            mSystemAudioMute = mute;
625            mSystemAudioVolume = volume;
626            // TODO: pass volume to service (audio service) after scale it to local volume level.
627            mService.setAudioStatus(mute, volume);
628        }
629    }
630
631    @ServiceThreadOnly
632    void changeVolume(int curVolume, int delta, int maxVolume) {
633        assertRunOnServiceThread();
634        if (delta == 0 || !isSystemAudioOn()) {
635            return;
636        }
637
638        int targetVolume = curVolume + delta;
639        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
640        synchronized (mLock) {
641            // If new volume is the same as current system audio volume, just ignore it.
642            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
643            if (cecVolume == mSystemAudioVolume) {
644                // Update tv volume with system volume value.
645                mService.setAudioStatus(false,
646                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
647                return;
648            }
649        }
650
651        // Remove existing volume action.
652        removeAction(VolumeControlAction.class);
653
654        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
655        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
656                cecVolume, delta > 0));
657    }
658
659    @ServiceThreadOnly
660    void changeMute(boolean mute) {
661        assertRunOnServiceThread();
662        if (!isSystemAudioOn()) {
663            return;
664        }
665
666        // Remove existing volume action.
667        removeAction(VolumeControlAction.class);
668        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
669        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
670    }
671
672    private boolean isSystemAudioOn() {
673        if (getAvrDeviceInfo() == null) {
674            return false;
675        }
676
677        synchronized (mLock) {
678            return mSystemAudioMode;
679        }
680    }
681
682    @Override
683    @ServiceThreadOnly
684    protected boolean handleInitiateArc(HdmiCecMessage message) {
685        assertRunOnServiceThread();
686        // In case where <Initiate Arc> is started by <Request ARC Initiation>
687        // need to clean up RequestArcInitiationAction.
688        removeAction(RequestArcInitiationAction.class);
689        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
690                message.getSource(), true);
691        addAndStartAction(action);
692        return true;
693    }
694
695    @Override
696    @ServiceThreadOnly
697    protected boolean handleTerminateArc(HdmiCecMessage message) {
698        assertRunOnServiceThread();
699        // In case where <Terminate Arc> is started by <Request ARC Termination>
700        // need to clean up RequestArcInitiationAction.
701        // TODO: check conditions of power status by calling is_connected api
702        // to be added soon.
703        removeAction(RequestArcTerminationAction.class);
704        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
705                message.getSource(), false);
706        addAndStartAction(action);
707        return true;
708    }
709
710    @Override
711    @ServiceThreadOnly
712    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
713        assertRunOnServiceThread();
714        if (!isMessageForSystemAudio(message)) {
715            return false;
716        }
717        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
718                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
719        addAndStartAction(action);
720        return true;
721    }
722
723    @Override
724    @ServiceThreadOnly
725    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
726        assertRunOnServiceThread();
727        if (!isMessageForSystemAudio(message)) {
728            return false;
729        }
730        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
731        return true;
732    }
733
734    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
735        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
736                || message.getDestination() != HdmiCec.ADDR_TV
737                || getAvrDeviceInfo() == null) {
738            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
739            return false;
740        }
741        return true;
742    }
743
744    /**
745     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
746     * logical address as new device info's.
747     *
748     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
749     *
750     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
751     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
752     *         that has the same logical address as new one has.
753     */
754    @ServiceThreadOnly
755    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
756        assertRunOnServiceThread();
757        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
758        if (oldDeviceInfo != null) {
759            removeDeviceInfo(deviceInfo.getLogicalAddress());
760        }
761        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
762        updateSafeDeviceInfoList();
763        return oldDeviceInfo;
764    }
765
766    /**
767     * Remove a device info corresponding to the given {@code logicalAddress}.
768     * It returns removed {@link HdmiCecDeviceInfo} if exists.
769     *
770     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
771     *
772     * @param logicalAddress logical address of device to be removed
773     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
774     */
775    @ServiceThreadOnly
776    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
777        assertRunOnServiceThread();
778        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
779        if (deviceInfo != null) {
780            mDeviceInfos.remove(logicalAddress);
781        }
782        updateSafeDeviceInfoList();
783        return deviceInfo;
784    }
785
786    /**
787     * Return a list of all {@link HdmiCecDeviceInfo}.
788     *
789     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
790     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
791     */
792    @ServiceThreadOnly
793    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
794        assertRunOnServiceThread();
795        if (includelLocalDevice) {
796            return HdmiUtils.sparseArrayToList(mDeviceInfos);
797        } else {
798            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
799            for (int i = 0; i < mDeviceInfos.size(); ++i) {
800                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
801                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
802                    infoList.add(info);
803                }
804            }
805            return infoList;
806        }
807    }
808
809    /**
810     * Return a list of  {@link HdmiCecDeviceInfo}.
811     *
812     * @param includeLocalDevice whether to include local device in result.
813     */
814    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
815        synchronized (mLock) {
816            if (includeLocalDevice) {
817                return mSafeAllDeviceInfos;
818            } else {
819                return mSafeExternalDeviceInfos;
820            }
821        }
822    }
823
824    @ServiceThreadOnly
825    private void updateSafeDeviceInfoList() {
826        assertRunOnServiceThread();
827        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
828        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
829        synchronized (mLock) {
830            mSafeAllDeviceInfos = copiedDevices;
831            mSafeExternalDeviceInfos = externalDeviceInfos;
832        }
833    }
834
835    @ServiceThreadOnly
836    private boolean isLocalDeviceAddress(int address) {
837        assertRunOnServiceThread();
838        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
839            if (device.isAddressOf(address)) {
840                return true;
841            }
842        }
843        return false;
844    }
845
846    @ServiceThreadOnly
847    HdmiCecDeviceInfo getAvrDeviceInfo() {
848        assertRunOnServiceThread();
849        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
850    }
851
852    /**
853     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
854     *
855     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
856     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
857     *
858     * @param logicalAddress logical address to be retrieved
859     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
860     *         Returns null if no logical address matched
861     */
862    @ServiceThreadOnly
863    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
864        assertRunOnServiceThread();
865        return mDeviceInfos.get(logicalAddress);
866    }
867
868    boolean hasSystemAudioDevice() {
869        return getSafeAvrDeviceInfo() != null;
870    }
871
872    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
873        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
874    }
875
876    /**
877     * Thread safe version of {@link #getDeviceInfo(int)}.
878     *
879     * @param logicalAddress logical address to be retrieved
880     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
881     *         Returns null if no logical address matched
882     */
883    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
884        synchronized (mLock) {
885            return mSafeAllDeviceInfos.get(logicalAddress);
886        }
887    }
888
889    /**
890     * Called when a device is newly added or a new device is detected.
891     *
892     * @param info device info of a new device.
893     */
894    @ServiceThreadOnly
895    final void addCecDevice(HdmiCecDeviceInfo info) {
896        assertRunOnServiceThread();
897        addDeviceInfo(info);
898        if (info.getLogicalAddress() == mAddress) {
899            // The addition of TV device itself should not be notified.
900            return;
901        }
902        mService.invokeDeviceEventListeners(info, true);
903    }
904
905    /**
906     * Called when a device is removed or removal of device is detected.
907     *
908     * @param address a logical address of a device to be removed
909     */
910    @ServiceThreadOnly
911    final void removeCecDevice(int address) {
912        assertRunOnServiceThread();
913        HdmiCecDeviceInfo info = removeDeviceInfo(address);
914        handleRemoveActiveRoutingPath(info.getPhysicalAddress());
915        mCecMessageCache.flushMessagesFrom(address);
916        mService.invokeDeviceEventListeners(info, false);
917    }
918
919    private void handleRemoveActiveRoutingPath(int path) {
920        // Seq #23
921        if (isTailOfActivePath(path, getActivePath())) {
922            removeAction(RoutingControlAction.class);
923            int newPath = mService.portIdToPath(getActivePortId());
924            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
925                    mAddress, getActivePath(), newPath));
926            addAndStartAction(new RoutingControlAction(this, getActivePortId(), null));
927        }
928    }
929
930    @ServiceThreadOnly
931    void routingAtEnableTime() {
932        assertRunOnServiceThread();
933        // Seq #24
934        if (getActivePortId() != HdmiConstants.INVALID_PORT_ID) {
935            // TODO: Check if TV was not powered on due to <Text/Image View On>,
936            //       TV is not in Preset Installation mode, not in initial setup mode, not
937            //       in Software updating mode, not in service mode, for following actions.
938            removeAction(RoutingControlAction.class);
939            int newPath = mService.portIdToPath(getActivePortId());
940            mService.sendCecCommand(
941                    HdmiCecMessageBuilder.buildRoutingChange(mAddress, getActivePath(), newPath));
942            addAndStartAction(new RoutingControlAction(this, getActivePortId(), null));
943        } else {
944            int activePath = mService.getPhysicalAddress();
945            setActivePath(activePath);
946            // TODO: Do following only when TV was not powered on due to <Text/Image View On>.
947            mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, activePath));
948        }
949    }
950
951    /**
952     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
953     * the given routing path. CEC devices use routing path for its physical address to
954     * describe the hierarchy of the devices in the network.
955     *
956     * @param path routing path or physical address
957     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
958     */
959    @ServiceThreadOnly
960    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
961        assertRunOnServiceThread();
962        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
963            if (info.getPhysicalAddress() == path) {
964                return info;
965            }
966        }
967        return null;
968    }
969
970    /**
971     * Whether a device of the specified physical address and logical address exists
972     * in a device info list. However, both are minimal condition and it could
973     * be different device from the original one.
974     *
975     * @param logicalAddress logical address of a device to be searched
976     * @param physicalAddress physical address of a device to be searched
977     * @return true if exist; otherwise false
978     */
979    @ServiceThreadOnly
980    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
981        assertRunOnServiceThread();
982        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
983        if (device == null) {
984            return false;
985        }
986        return device.getPhysicalAddress() == physicalAddress;
987    }
988
989    @Override
990    @ServiceThreadOnly
991    void onHotplug(int portId, boolean connected) {
992        assertRunOnServiceThread();
993
994        // Tv device will have permanent HotplugDetectionAction.
995        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
996        if (!hotplugActions.isEmpty()) {
997            // Note that hotplug action is single action running on a machine.
998            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
999            hotplugActions.get(0).pollAllDevicesNow();
1000        }
1001    }
1002
1003    @ServiceThreadOnly
1004    void setAutoDeviceOff(boolean enabled) {
1005        assertRunOnServiceThread();
1006        mAutoDeviceOff = enabled;
1007    }
1008
1009    @Override
1010    @ServiceThreadOnly
1011    protected void onTransitionToStandby(boolean initiatedByCec) {
1012        assertRunOnServiceThread();
1013        // Remove any repeated working actions.
1014        // HotplugDetectionAction will be reinstated during the wake up process.
1015        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1016        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1017        removeAction(HotplugDetectionAction.class);
1018        checkIfPendingActionsCleared();
1019    }
1020
1021    @Override
1022    @ServiceThreadOnly
1023    protected void onStandBy(boolean initiatedByCec) {
1024        assertRunOnServiceThread();
1025        // Seq #11
1026        if (!mService.isControlEnabled()) {
1027            return;
1028        }
1029        if (!initiatedByCec) {
1030            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1031                    mAddress, HdmiCec.ADDR_BROADCAST));
1032        }
1033    }
1034
1035    @Override
1036    @ServiceThreadOnly
1037    protected boolean handleStandby(HdmiCecMessage message) {
1038        assertRunOnServiceThread();
1039        // Seq #12
1040        // Tv accepts directly addressed <Standby> only.
1041        if (message.getDestination() == mAddress) {
1042            super.handleStandby(message);
1043        }
1044        return false;
1045    }
1046
1047    boolean isProhibitMode() {
1048        return mService.isProhibitMode();
1049    }
1050}
1051