/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.hdmi;

import android.annotation.CallSuper;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.os.Binder;
import android.os.Handler;
import android.os.PowerManager;
import android.os.SystemProperties;
import android.sysprop.HdmiProperties;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.LocalePicker;
import com.android.internal.app.LocalePicker.LocaleInfo;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
import com.android.server.hdmi.HdmiControlService.SendMessageCallback;

import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

/**
 * Represent a logical device of type Playback residing in Android system.
 */
public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
    private static final String TAG = "HdmiCecLocalDevicePlayback";

    // How long to wait after hotplug out before possibly going to Standby.
    @VisibleForTesting
    static final long STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS = 30_000;

    // How long to wait on active source lost before possibly going to Standby.
    @VisibleForTesting
    static final long STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS = 30_000;

    // How long to wait after losing active source, before launching the pop-up that allows the user
    // to keep the device as the current active source.
    // We do this to prevent an unnecessary pop-up from being displayed when we lose and regain
    // active source within this timeout.
    static final long POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS = 5_000;

    // Used to keep the device awake while it is the active source. For devices that
    // cannot wake up via CEC commands, this address the inconvenience of having to
    // turn them on. True by default, and can be disabled (i.e. device can go to sleep
    // in active device status) by explicitly setting the system property
    // persist.sys.hdmi.keep_awake to false.
    // Lazily initialized - should call getWakeLock() to get the instance.
    private ActiveWakeLock mWakeLock;

    // Handler for queueing a delayed Standby runnable after hotplug out.
    private Handler mDelayedStandbyHandler;

    // Handler for queueing a delayed Standby runnable after active source lost and after the pop-up
    // on active source lost was displayed.
    Handler mDelayedStandbyOnActiveSourceLostHandler;

    // Handler for queueing a delayed runnable that triggers a pop-up notification on active source
    // lost.
    private Handler mDelayedPopupOnActiveSourceLostHandler;

    private boolean mIsActiveSourceLostPopupLaunched;

    // Determines what action should be taken upon receiving Routing Control messages.
    @VisibleForTesting
    protected HdmiProperties.playback_device_action_on_routing_control_values
            mPlaybackDeviceActionOnRoutingControl = HdmiProperties
                    .playback_device_action_on_routing_control()
                    .orElse(HdmiProperties.playback_device_action_on_routing_control_values.NONE);

    HdmiCecLocalDevicePlayback(HdmiControlService service) {
        super(service, HdmiDeviceInfo.DEVICE_PLAYBACK);

        mDelayedStandbyHandler = new Handler(service.getServiceLooper());
        mDelayedStandbyOnActiveSourceLostHandler = new Handler(service.getServiceLooper());
        mDelayedPopupOnActiveSourceLostHandler = new Handler(service.getServiceLooper());
        mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
        mIsActiveSourceLostPopupLaunched = false;
    }

    @Override
    @ServiceThreadOnly
    protected void onAddressAllocated(int logicalAddress, int reason) {
        assertRunOnServiceThread();
        if (reason == mService.INITIATED_BY_ENABLE_CEC) {
            mService.setAndBroadcastActiveSource(mService.getPhysicalAddress(),
                    getDeviceInfo().getDeviceType(), Constants.ADDR_BROADCAST,
                    "HdmiCecLocalDevicePlayback#onAddressAllocated()");
        }
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                        getDeviceInfo().getLogicalAddress(),
                        mService.getPhysicalAddress(),
                        mDeviceType));
        mService.sendCecCommand(
                HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
                        getDeviceInfo().getLogicalAddress(), mService.getVendorId()));
        // Actively send out an OSD name to the TV to update the TV panel in case the TV
        // does not query the OSD name on time. This is not a required behavior by the spec.
        // It is used for some TVs that need the OSD name update but don't query it themselves.
        buildAndSendSetOsdName(Constants.ADDR_TV);
        if (mService.audioSystem() == null) {
            // If current device is not a functional audio system device,
            // send message to potential audio system device in the system to get the system
            // audio mode status. If no response, set to false.
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildGiveSystemAudioModeStatus(
                            getDeviceInfo().getLogicalAddress(), Constants.ADDR_AUDIO_SYSTEM),
                    new SendMessageCallback() {
                        @Override
                        public void onSendCompleted(int error) {
                            // In consideration of occasional transmission failures.
                            if (error == SendMessageResult.NACK) {
                                HdmiLogger.debug(
                                        "AVR did not respond to <Give System Audio Mode Status>");
                                mService.setSystemAudioActivated(false);
                            }
                        }
                    });
        }
        launchDeviceDiscovery();
        startQueuedActions();
    }

    @ServiceThreadOnly
    private void launchDeviceDiscovery() {
        assertRunOnServiceThread();
        clearDeviceInfoList();
        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
                new DeviceDiscoveryAction.DeviceDiscoveryCallback() {
                    @Override
                    public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
                        for (HdmiDeviceInfo info : deviceInfos) {
                            mService.getHdmiCecNetwork().addCecDevice(info);
                        }

                        // Since we removed all devices when it starts and device discovery
                        // action does not poll local devices, we should put device info of
                        // local device manually here.
                        for (HdmiCecLocalDevice device : mService.getAllCecLocalDevices()) {
                            mService.getHdmiCecNetwork().addCecDevice(device.getDeviceInfo());
                        }

                        if (!hasAction(HotplugDetectionAction.class)) {
                            addAndStartAction(
                                    new HotplugDetectionAction(
                                            HdmiCecLocalDevicePlayback.this));
                        }

                        if (mService.isHdmiControlEnhancedBehaviorFlagEnabled()) {
                            if (mService.getHdmiCecConfig().getStringValue(
                                    HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)
                                    .equals(HdmiControlManager
                                            .POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW)
                                    && !hasAction(PowerStatusMonitorActionFromPlayback.class)) {
                                addAndStartAction(
                                        new PowerStatusMonitorActionFromPlayback(
                                                HdmiCecLocalDevicePlayback.this));
                            }
                        }
                    }
                });
        addAndStartAction(action, true);
    }

    @Override
    @ServiceThreadOnly
    protected int getPreferredAddress() {
        assertRunOnServiceThread();
        return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
                Constants.ADDR_UNREGISTERED);
    }

    @Override
    @ServiceThreadOnly
    protected void setPreferredAddress(int addr) {
        assertRunOnServiceThread();
        mService.writeStringSystemProperty(Constants.PROPERTY_PREFERRED_ADDRESS_PLAYBACK,
                String.valueOf(addr));
    }

    /**
     * Performs the action 'device select' or 'one touch play' initiated by a Playback device.
     *
     * @param id id of HDMI device to select
     * @param callback callback object to report the result with
     */
    @ServiceThreadOnly
    void deviceSelect(int id, IHdmiControlCallback callback) {
        assertRunOnServiceThread();
        if (id == getDeviceInfo().getId()) {
            mService.oneTouchPlay(callback);
            return;
        }
        HdmiDeviceInfo targetDevice = mService.getHdmiCecNetwork().getDeviceInfo(id);
        if (targetDevice == null) {
            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
            return;
        }
        int targetAddress = targetDevice.getLogicalAddress();
        if (isAlreadyActiveSource(targetDevice, targetAddress, callback)) {
            return;
        }
        if (!mService.isCecControlEnabled()) {
            setActiveSource(targetDevice, "HdmiCecLocalDevicePlayback#deviceSelect()");
            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
            return;
        }
        List<DeviceSelectActionFromPlayback> actions = getActions(
                DeviceSelectActionFromPlayback.class);
        if (!actions.isEmpty()) {
            DeviceSelectActionFromPlayback action = actions.get(0);
            if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
                return;
            }
        }
        addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback),
                true);
    }

    @Override
    @ServiceThreadOnly
    void onHotplug(int portId, boolean connected) {
        assertRunOnServiceThread();
        mCecMessageCache.flushAll();

        if (connected) {
            mDelayedStandbyHandler.removeCallbacksAndMessages(null);
        } else {
            // We'll not invalidate the active source on the hotplug event to pass CETC 11.2.2-2 ~ 3
            getWakeLock().release();
            mService.getHdmiCecNetwork().removeDevicesConnectedToPort(portId);

            mDelayedStandbyHandler.removeCallbacksAndMessages(null);
            mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
                    STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
        }
    }

    /**
     * Runnable for going to Standby if the device has been inactive for a certain amount of time.
     * Posts a new instance of itself as a delayed message if the device was active.
     */
    private class DelayedStandbyRunnable implements Runnable {
        @Override
        public void run() {
            if (mService.getPowerManagerInternal().wasDeviceIdleFor(
                    STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS)) {
                mService.standby();
            } else {
                mDelayedStandbyHandler.postDelayed(new DelayedStandbyRunnable(),
                        STANDBY_AFTER_HOTPLUG_OUT_DELAY_MS);
            }
        }
    }

    private class DelayedStandbyOnActiveSourceLostRunnable implements Runnable {
        @Override
        public void run() {
            if (!isActiveSource()) {
                mService.standby();
                mIsActiveSourceLostPopupLaunched = false;
            }
        }
    }

    @ServiceThreadOnly
    void dismissUiOnActiveSourceStatusRecovered() {
        assertRunOnServiceThread();
        Intent intent = new Intent(HdmiControlManager.ACTION_ON_ACTIVE_SOURCE_RECOVERED_DISMISS_UI);
        mIsActiveSourceLostPopupLaunched = false;
        mService.sendBroadcastAsUser(intent);
    }

    @Override
    @ServiceThreadOnly
    protected void onStandby(boolean initiatedByCec, int standbyAction,
            StandbyCompletedCallback callback) {
        assertRunOnServiceThread();
        if (!mService.isCecControlEnabled()) {
            invokeStandbyCompletedCallback(callback);
            return;
        }
        boolean wasActiveSource = isActiveSource();
        // Invalidate the internal active source record when going to standby
        mService.setActiveSource(Constants.ADDR_INVALID, Constants.INVALID_PHYSICAL_ADDRESS,
                "HdmiCecLocalDevicePlayback#onStandby()");
        if (!wasActiveSource) {
            invokeStandbyCompletedCallback(callback);
            return;
        }
        SendMessageCallback sendMessageCallback = new SendMessageCallback() {
            @Override
            public void onSendCompleted(int error) {
                invokeStandbyCompletedCallback(callback);
            }
        };
        if (initiatedByCec) {
            mService.sendCecCommand(
                    HdmiCecMessageBuilder.buildInactiveSource(
                            getDeviceInfo().getLogicalAddress(), mService.getPhysicalAddress()),
                    sendMessageCallback);
            return;
        }
        switch (standbyAction) {
            case HdmiControlService.STANDBY_SCREEN_OFF:
                // Get latest setting value
                @HdmiControlManager.PowerControlMode
                String powerControlMode = mService.getHdmiCecConfig().getStringValue(
                        HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
                switch (powerControlMode) {
                    case HdmiControlManager.POWER_CONTROL_MODE_TV:
                        mService.sendCecCommand(
                                HdmiCecMessageBuilder.buildStandby(
                                        getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV),
                                sendMessageCallback);
                        break;
                    case HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM:
                        mService.sendCecCommand(
                                HdmiCecMessageBuilder.buildStandby(
                                        getDeviceInfo().getLogicalAddress(), Constants.ADDR_TV));
                        mService.sendCecCommand(
                                HdmiCecMessageBuilder.buildStandby(
                                        getDeviceInfo().getLogicalAddress(),
                                        Constants.ADDR_AUDIO_SYSTEM), sendMessageCallback);
                        break;
                    case HdmiControlManager.POWER_CONTROL_MODE_BROADCAST:
                        mService.sendCecCommand(
                                HdmiCecMessageBuilder.buildStandby(
                                        getDeviceInfo().getLogicalAddress(),
                                        Constants.ADDR_BROADCAST), sendMessageCallback);
                        break;
                    case HdmiControlManager.POWER_CONTROL_MODE_NONE:
                        mService.sendCecCommand(
                                HdmiCecMessageBuilder.buildInactiveSource(
                                        getDeviceInfo().getLogicalAddress(),
                                        mService.getPhysicalAddress()), sendMessageCallback);
                        break;
                }
                break;
            case HdmiControlService.STANDBY_SHUTDOWN:
                // ACTION_SHUTDOWN is taken as a signal to power off all the devices.
                mService.sendCecCommand(
                        HdmiCecMessageBuilder.buildStandby(
                                getDeviceInfo().getLogicalAddress(), Constants.ADDR_BROADCAST),
                        sendMessageCallback);
                break;
        }
    }

    @Override
    @ServiceThreadOnly
    protected void onInitializeCecComplete(int initiatedBy) {
        if (initiatedBy != HdmiControlService.INITIATED_BY_SCREEN_ON) {
            return;
        }
        @HdmiControlManager.PowerControlMode
        String powerControlMode = mService.getHdmiCecConfig().getStringValue(
                HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
        if (powerControlMode.equals(HdmiControlManager.POWER_CONTROL_MODE_NONE)) {
            return;
        }
        oneTouchPlay(new IHdmiControlCallback.Stub() {
            @Override
            public void onComplete(int result) {
                if (result != HdmiControlManager.RESULT_SUCCESS) {
                    Slog.w(TAG, "Failed to complete One Touch Play. result=" + result);
                }
            }
        });
    }

    @Override
    @CallSuper
    @ServiceThreadOnly
    @VisibleForTesting
    protected void setActiveSource(int logicalAddress, int physicalAddress, String caller) {
        assertRunOnServiceThread();
        super.setActiveSource(logicalAddress, physicalAddress, caller);
        if (isActiveSource()) {
            getWakeLock().acquire();
        } else {
            getWakeLock().release();
        }
    }

    @ServiceThreadOnly
    private ActiveWakeLock getWakeLock() {
        assertRunOnServiceThread();
        if (mWakeLock == null) {
            if (SystemProperties.getBoolean(Constants.PROPERTY_KEEP_AWAKE, true)) {
                mWakeLock = new SystemWakeLock();
            } else {
                // Create a stub lock object that doesn't do anything about wake lock,
                // hence allows the device to go to sleep even if it's the active source.
                mWakeLock = new ActiveWakeLock() {
                    @Override
                    public void acquire() { }
                    @Override
                    public void release() { }
                    @Override
                    public boolean isHeld() { return false; }
                };
                HdmiLogger.debug("No wakelock is used to keep the display on.");
            }
        }
        return mWakeLock;
    }

    @Override
    protected boolean canGoToStandby() {
        return !getWakeLock().isHeld();
    }

    @Override
    @ServiceThreadOnly
    protected void onActiveSourceLost() {
        assertRunOnServiceThread();
        mService.pauseActiveMediaSessions();
        switch (mService.getHdmiCecConfig().getStringValue(
                    HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST)) {
            case HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW:
                mDelayedPopupOnActiveSourceLostHandler.removeCallbacksAndMessages(null);
                mDelayedPopupOnActiveSourceLostHandler.postDelayed(
                        new Runnable() {
                            @Override
                            public void run() {
                                if (isActiveSource()) {
                                    return;
                                }

                                if (getActiveSource().logicalAddress != Constants.ADDR_TV) {
                                    startHdmiCecActiveSourceLostActivity();
                                    mDelayedStandbyOnActiveSourceLostHandler
                                            .removeCallbacksAndMessages(null);
                                    mDelayedStandbyOnActiveSourceLostHandler.postDelayed(
                                            new DelayedStandbyOnActiveSourceLostRunnable(),
                                            STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
                                    return;
                                }

                                // We observed specific TV panels (old models) that send faulty CEC
                                // source changing messages, especially during wake-up.
                                // This request helps to check if the TV correctly asserted active
                                // source or not. If the request times out, active source is
                                // asserted by the local device.
                                addAndStartAction(new RequestActiveSourceAction(mService.playback(),
                                        new IHdmiControlCallback.Stub() {
                                    @Override
                                    public void onComplete(int result) {
                                        // If a device answers to <Request Active Source>, the
                                        // pop-up should be triggered.
                                        // During this action, the TV can switch to an HDMI input
                                        // with a non-CEC capable device that won't be able to
                                        // answer this request.
                                        // In this case, the known active source would be
                                        // represented by a valid physical address, but invalid
                                        // logical address. The pop-up will be shown and the local
                                        // device will not assert active source.
                                        if (result == HdmiControlManager.RESULT_SUCCESS
                                                || getActiveSource().logicalAddress
                                                != Constants.ADDR_TV) {
                                                startHdmiCecActiveSourceLostActivity();
                                                mDelayedStandbyOnActiveSourceLostHandler
                                                        .removeCallbacksAndMessages(null);
                                                mDelayedStandbyOnActiveSourceLostHandler
                                                        .postDelayed(
                                                                new DelayedStandbyOnActiveSourceLostRunnable(),
                                                        STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
                                        } else {
                                            // The request times out and the local device is not
                                            // active source, but the TV previously asserted active
                                            // source.
                                            if (getActiveSource().logicalAddress
                                                    == Constants.ADDR_TV) {
                                                mService.setAndBroadcastActiveSource(
                                                        mService.getPhysicalAddress(),
                                                        getDeviceInfo().getDeviceType(),
                                                        Constants.ADDR_BROADCAST,
                                                        "RequestActiveSourceAction#RESULT_TIMEOUT");
                                            }
                                        }
                                    }}));
                            }
                        }, POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS);
                return;
            case HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_NONE:
                return;
        }
    }

    @VisibleForTesting
    @ServiceThreadOnly
    void startHdmiCecActiveSourceLostActivity() {
        final long identity = Binder.clearCallingIdentity();
        try {
            Context context = mService.getContext();
            Intent intent = new Intent();
            intent.setComponent(
                    ComponentName.unflattenFromString(context.getResources().getString(
                            com.android.internal.R.string.config_hdmiCecActiveSourceLostActivity
                    )));
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivityAsUser(intent, context.getUser());
            mIsActiveSourceLostPopupLaunched = true;
        } catch (ActivityNotFoundException e) {
            Slog.e(TAG, "Unable to start HdmiCecActiveSourceLostActivity");
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleUserControlPressed(HdmiCecMessage message) {
        assertRunOnServiceThread();
        wakeUpIfActiveSource();
        return super.handleUserControlPressed(message);
    }

    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleSetMenuLanguage(HdmiCecMessage message) {
        assertRunOnServiceThread();
        if (mService.getHdmiCecConfig().getIntValue(
                HdmiControlManager.CEC_SETTING_NAME_SET_MENU_LANGUAGE)
                    == HdmiControlManager.SET_MENU_LANGUAGE_DISABLED) {
            return Constants.ABORT_UNRECOGNIZED_OPCODE;
        }

        try {
            String iso3Language = new String(message.getParams(), 0, 3, "US-ASCII");
            Locale currentLocale = mService.getContext().getResources().getConfiguration().locale;
            String curIso3Language = mService.localeToMenuLanguage(currentLocale);
            HdmiLogger.debug("handleSetMenuLanguage " + iso3Language + " cur:" + curIso3Language);
            if (curIso3Language.equals(iso3Language)) {
                // Do not switch language if the new language is the same as the current one.
                // This helps avoid accidental country variant switching from en_US to en_AU
                // due to the limitation of CEC. See the warning below.
                return Constants.HANDLED;
            }

            // Don't use Locale.getAvailableLocales() since it returns a locale
            // which is not available on Settings.
            final List<LocaleInfo> localeInfos = LocalePicker.getAllAssetLocales(
                    mService.getContext(), false);
            for (LocaleInfo localeInfo : localeInfos) {
                if (mService.localeToMenuLanguage(localeInfo.getLocale()).equals(iso3Language)) {
                    // WARNING: CEC adopts ISO/FDIS-2 for language code, while Android requires
                    // additional country variant to pinpoint the locale. This keeps the right
                    // locale from being chosen. 'eng' in the CEC command, for instance,
                    // will always be mapped to en-AU among other variants like en-US, en-GB,
                    // an en-IN, which may not be the expected one.
                    startSetMenuLanguageActivity(localeInfo.getLocale());
                    return Constants.HANDLED;
                }
            }
            Slog.w(TAG, "Can't handle <Set Menu Language> of " + iso3Language);
            return Constants.ABORT_INVALID_OPERAND;
        } catch (UnsupportedEncodingException e) {
            Slog.w(TAG, "Can't handle <Set Menu Language>", e);
            return Constants.ABORT_INVALID_OPERAND;
        }
    }

    private void startSetMenuLanguageActivity(Locale locale) {
        final long identity = Binder.clearCallingIdentity();
        try {
            Context context = mService.getContext();
            Intent intent = new Intent();
            intent.putExtra(HdmiControlManager.EXTRA_LOCALE, locale.toLanguageTag());
            intent.setComponent(
                    ComponentName.unflattenFromString(context.getResources().getString(
                            com.android.internal.R.string.config_hdmiCecSetMenuLanguageActivity)));
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivityAsUser(intent, context.getUser());
        } catch (ActivityNotFoundException e) {
            Slog.e(TAG, "unable to start HdmiCecSetMenuLanguageActivity");
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    @Constants.HandleMessageResult
    protected int handleSetSystemAudioMode(HdmiCecMessage message) {
        // System Audio Mode only turns on/off when Audio System broadcasts on/off message.
        // For device with type 4 and 5, it can set system audio mode on/off
        // when there is another audio system device connected into the system first.
        if (message.getDestination() != Constants.ADDR_BROADCAST
                || message.getSource() != Constants.ADDR_AUDIO_SYSTEM
                || mService.audioSystem() != null) {
            return Constants.HANDLED;
        }
        boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message);
        if (mService.isSystemAudioActivated() != setSystemAudioModeOn) {
            mService.setSystemAudioActivated(setSystemAudioModeOn);
        }
        return Constants.HANDLED;
    }

    @Override
    @Constants.HandleMessageResult
    protected int handleSystemAudioModeStatus(HdmiCecMessage message) {
        // Only directly addressed System Audio Mode Status message can change internal
        // system audio mode status.
        if (message.getDestination() == getDeviceInfo().getLogicalAddress()
                && message.getSource() == Constants.ADDR_AUDIO_SYSTEM) {
            boolean setSystemAudioModeOn = HdmiUtils.parseCommandParamSystemAudioStatus(message);
            if (mService.isSystemAudioActivated() != setSystemAudioModeOn) {
                mService.setSystemAudioActivated(setSystemAudioModeOn);
            }
        }
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRoutingChange(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams(), 2);
        handleRoutingChangeAndInformation(physicalAddress, message);
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    @Constants.HandleMessageResult
    protected int handleRoutingInformation(HdmiCecMessage message) {
        assertRunOnServiceThread();
        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
        HdmiDeviceInfo sourceDevice = mService.getHdmiCecNetwork()
                .getCecDeviceInfo(message.getSource());
        // Ignore <Routing Information> messages pointing to the same physical address as the
        // message sender. In this case, we shouldn't consider the sender to be the active source.
        // See more b/321771821#comment7.
        if (sourceDevice != null
                && sourceDevice.getLogicalAddress() != Constants.ADDR_TV
                && sourceDevice.getPhysicalAddress() == physicalAddress) {
            Slog.d(TAG, "<Routing Information> is ignored, it is pointing to the same physical"
                    + " address as the message sender");
            return Constants.HANDLED;
        }
        handleRoutingChangeAndInformation(physicalAddress, message);
        return Constants.HANDLED;
    }

    @Override
    @ServiceThreadOnly
    protected void handleRoutingChangeAndInformation(int physicalAddress, HdmiCecMessage message) {
        assertRunOnServiceThread();
        // If the device is active source and received a <Routing Change> or <Routing Information>
        // message to a physical address in the same active path do not change the Active Source
        // status.
        // E.g. TV [0.0.0.0] ------ Switch [2.0.0.0] ------ OTT [2.1.0.0] (Active Source)
        // TV sends <Routing Change>[2.0.0.0] -> OTT is still Active Source
        // TV sends <Routing Change>[0.0.0.0] -> OTT is not Active Source anymore.
        // TV sends <Routing Change>[3.0.0.0] -> OTT is not Active Source anymore.
        if (HdmiUtils.isInActiveRoutingPath(mService.getPhysicalAddress(), physicalAddress)
                && physicalAddress != Constants.TV_PHYSICAL_ADDRESS
                && isActiveSource()) {
            return;
        }
        if (physicalAddress != mService.getPhysicalAddress()) {
            setActiveSource(physicalAddress,
                    "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
            return;
        }
        if (!isActiveSource()) {
            // If routing is changed to the device while Active Source, don't invalidate the
            // Active Source
            setActiveSource(physicalAddress,
                    "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
        }
        dismissUiOnActiveSourceStatusRecovered();
        switch (mPlaybackDeviceActionOnRoutingControl) {
            case WAKE_UP_AND_SEND_ACTIVE_SOURCE:
                setAndBroadcastActiveSource(message, physicalAddress,
                        "HdmiCecLocalDevicePlayback#handleRoutingChangeAndInformation()");
                break;
            case WAKE_UP_ONLY:
                mService.wakeUp();
                break;
            case NONE:
                break;
        }
    }

    /**
     * Called after logical address allocation is finished, allowing a local device to react to
     * messages in the buffer before they are processed. This method may be used to cancel deferred
     * actions.
     */
    @Override
    protected void preprocessBufferedMessages(List<HdmiCecMessage> bufferedMessages) {
        for (HdmiCecMessage message: bufferedMessages) {
            // Prevent the device from broadcasting <Active Source> message if the active path
            // changed during address allocation.
            if (message.getOpcode() == Constants.MESSAGE_ROUTING_CHANGE
                    || message.getOpcode() == Constants.MESSAGE_SET_STREAM_PATH
                    || message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
                removeAction(ActiveSourceAction.class);
                removeAction(OneTouchPlayAction.class);
                return;
            }
        }
    }

    @Override
    protected int findKeyReceiverAddress() {
        return Constants.ADDR_TV;
    }

    @Override
    protected int findAudioReceiverAddress() {
        if (mService.isSystemAudioActivated()) {
            return Constants.ADDR_AUDIO_SYSTEM;
        }
        return Constants.ADDR_TV;
    }

    boolean isActiveSourceLostPopupLaunched() {
        return mIsActiveSourceLostPopupLaunched;
    }

    void setIsActiveSourceLostPopupLaunched(boolean isActiveSourceLostPopupLaunched) {
        mIsActiveSourceLostPopupLaunched = isActiveSourceLostPopupLaunched;
    }

    @Override
    @ServiceThreadOnly
    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
        assertRunOnServiceThread();
        removeAction(DeviceDiscoveryAction.class);
        removeAction(HotplugDetectionAction.class);
        removeAction(NewDeviceAction.class);
        removeAction(PowerStatusMonitorActionFromPlayback.class);
        removeAction(RequestActiveSourceAction.class);
        super.disableDevice(initiatedByCec, callback);
        clearDeviceInfoList();
        checkIfPendingActionsCleared();
    }

    @Override
    protected void dump(final IndentingPrintWriter pw) {
        super.dump(pw);
        pw.println("isActiveSource(): " + isActiveSource());
    }

    // Wrapper interface over PowerManager.WakeLock
    private interface ActiveWakeLock {
        void acquire();
        void release();
        boolean isHeld();
    }

    private class SystemWakeLock implements ActiveWakeLock {
        private final WakeLockWrapper mWakeLock;
        public SystemWakeLock() {
            mWakeLock = mService.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
            mWakeLock.setReferenceCounted(false);
        }

        @Override
        public void acquire() {
            mWakeLock.acquire();
            HdmiLogger.debug("active source: %b. Wake lock acquired", isActiveSource());
        }

        @Override
        public void release() {
            mWakeLock.release();
            HdmiLogger.debug("Wake lock released");
        }

        @Override
        public boolean isHeld() {
            return mWakeLock.isHeld();
        }
    }
}
