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