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