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