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