/*
 * Copyright (C) 2018 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.wifi;

import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
import static android.telephony.TelephonyManager.CALL_STATE_RINGING;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

import com.android.modules.utils.HandlerExecutor;
import com.android.wifi.resources.R;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;

/**
 * This class provides the Support for SAR to control WiFi TX power limits.
 * It deals with the following:
 * - Tracking the STA state through calls from  the ClientModeManager.
 * - Tracking the SAP state through calls from SoftApManager
 * - Tracking the Scan-Only state through ScanOnlyModeManager
 * - Tracking the state of the Cellular calls or data.
 * - It constructs the sar info and send it towards the HAL
 */
public class SarManager {
    // Period for checking on voice steam active (in ms)
    private static final int CHECK_VOICE_STREAM_INTERVAL_MS = 5000;

    /**
     * @hide constants copied over from {@link AudioManager}
     * TODO(b/144250387): Migrate to public API
     */
    private static final String STREAM_DEVICES_CHANGED_ACTION =
            "android.media.STREAM_DEVICES_CHANGED_ACTION";
    private static final String EXTRA_VOLUME_STREAM_TYPE = "android.media.EXTRA_VOLUME_STREAM_TYPE";
    private static final String EXTRA_VOLUME_STREAM_DEVICES =
            "android.media.EXTRA_VOLUME_STREAM_DEVICES";
    private static final String EXTRA_PREV_VOLUME_STREAM_DEVICES =
            "android.media.EXTRA_PREV_VOLUME_STREAM_DEVICES";
    private static final int DEVICE_OUT_EARPIECE = 0x1;

    /* For Logging */
    private static final String TAG = "WifiSarManager";
    private boolean mVerboseLoggingEnabled = true;

    private final SarInfo mSarInfo;

    /* Configuration for SAR support */
    private boolean mSupportSarTxPowerLimit;
    private boolean mSupportSarVoiceCall;
    private boolean mSupportSarSoftAp;

    // Device starts with screen on
    private boolean mScreenOn = false;
    private boolean mIsVoiceStreamCheckEnabled = false;

    /**
     * Other parameters passed in or created in the constructor.
     */
    private final Context mContext;
    private final TelephonyManager mTelephonyManager;
    private final AudioManager mAudioManager;
    private final WifiPhoneStateListener mPhoneStateListener;
    private final WifiNative mWifiNative;
    private final Handler mHandler;

    /** Create new instance of SarManager. */
    SarManager(
            Context context,
            TelephonyManager telephonyManager,
            Looper looper,
            WifiNative wifiNative,
            WifiDeviceStateChangeManager wifiDeviceStateChangeManager) {
        mContext = context;
        mTelephonyManager = telephonyManager;
        mWifiNative = wifiNative;
        mAudioManager = mContext.getSystemService(AudioManager.class);
        mHandler = new Handler(looper);
        mPhoneStateListener = new WifiPhoneStateListener(looper);
        mSarInfo = new SarInfo();
        wifiDeviceStateChangeManager.registerStateChangeCallback(
                new WifiDeviceStateChangeManager.StateChangeCallback() {
                    @Override
                    public void onScreenStateChanged(boolean screenOn) {
                        handleScreenStateChanged(screenOn);
                    }
                });
    }

    /**
     * Handle boot completed, read config flags.
     */
    public void handleBootCompleted() {
        readSarConfigs();
        if (mSupportSarTxPowerLimit) {
            setSarConfigsInInfo();
            registerListeners();
            updateSarScenario();
        }
    }

    /**
     * Notify SarManager of screen status change
     */
    private void handleScreenStateChanged(boolean screenOn) {
        if (!mSupportSarVoiceCall) {
            return;
        }

        if (mScreenOn == screenOn) {
            return;
        }

        if (mVerboseLoggingEnabled) {
            Log.d(TAG, "handleScreenStateChanged: screenOn = " + screenOn);
        }

        mScreenOn = screenOn;

        // Only schedule a voice stream check if screen is turning on, and it is currently not
        // scheduled
        if (mScreenOn && !mIsVoiceStreamCheckEnabled) {
            mHandler.post(() -> {
                checkAudioDevice();
            });

            mIsVoiceStreamCheckEnabled = true;
        }
    }

    private boolean isVoiceCallOnEarpiece() {
        final AudioAttributes voiceCallAttr = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                .build();
        List<AudioDeviceAttributes> devices = mAudioManager.getDevicesForAttributes(voiceCallAttr);
        for (AudioDeviceAttributes device : devices) {
            if (device.getRole() == AudioDeviceAttributes.ROLE_OUTPUT
                    && device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
                return true;
            }
        }
        return false;
    }

    private boolean isVoiceCallStreamActive() {
        int mode = mAudioManager.getMode();
        return mode == AudioManager.MODE_IN_COMMUNICATION || mode == AudioManager.MODE_IN_CALL;
    }

    private void checkAudioDevice() {
        // First Check if audio stream is on
        boolean voiceStreamActive = isVoiceCallStreamActive();
        boolean earPieceActive;

        if (voiceStreamActive) {
            // Check on the audio route
            earPieceActive = isVoiceCallOnEarpiece();

            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "EarPiece active = " + earPieceActive);
            }
        } else {
            earPieceActive = false;
        }

        // If audio route has changed, update SAR
        if (earPieceActive != mSarInfo.isEarPieceActive) {
            mSarInfo.isEarPieceActive = earPieceActive;
            updateSarScenario();
        }

        // Now should we proceed with the checks
        if (!mScreenOn && !voiceStreamActive) {
            // No need to continue checking
            mIsVoiceStreamCheckEnabled = false;
        } else {
            // Schedule another check
            mHandler.postDelayed(() -> {
                checkAudioDevice();
            }, CHECK_VOICE_STREAM_INTERVAL_MS);
        }
    }

    private void readSarConfigs() {
        mSupportSarTxPowerLimit = mContext.getResources().getBoolean(
                R.bool.config_wifi_framework_enable_sar_tx_power_limit);
        /* In case SAR is disabled,
           then all SAR inputs are automatically disabled as well (irrespective of the config) */
        if (!mSupportSarTxPowerLimit) {
            mSupportSarVoiceCall = false;
            mSupportSarSoftAp = false;
            return;
        }

        /* Voice calls are supported when SAR is supported */
        mSupportSarVoiceCall = true;

        mSupportSarSoftAp = mContext.getResources().getBoolean(
                R.bool.config_wifi_framework_enable_soft_ap_sar_tx_power_limit);
    }

    private void setSarConfigsInInfo() {
        mSarInfo.sarVoiceCallSupported = mSupportSarVoiceCall;
        mSarInfo.sarSapSupported = mSupportSarSoftAp;
    }

    private void registerListeners() {
        if (mSupportSarVoiceCall) {
            /* Listen for Phone State changes */
            registerPhoneStateListener();
            registerVoiceStreamListener();
        }
    }

    private void registerVoiceStreamListener() {
        Log.i(TAG, "Registering for voice stream status");

        // Register for listening to transitions of change of voice stream devices
        IntentFilter filter = new IntentFilter();
        filter.addAction(STREAM_DEVICES_CHANGED_ACTION);

        mContext.registerReceiver(
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        boolean voiceStreamActive = isVoiceCallStreamActive();
                        if (!voiceStreamActive) {
                            // No need to proceed, there is no voice call ongoing
                            return;
                        }

                        String action = intent.getAction();
                        int streamType =
                                intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1);
                        int device = intent.getIntExtra(EXTRA_VOLUME_STREAM_DEVICES, -1);
                        int oldDevice = intent.getIntExtra(EXTRA_PREV_VOLUME_STREAM_DEVICES, -1);

                        if (streamType == AudioManager.STREAM_VOICE_CALL) {
                            boolean earPieceActive = mSarInfo.isEarPieceActive;
                            if (device == DEVICE_OUT_EARPIECE) {
                                if (mVerboseLoggingEnabled) {
                                    Log.d(TAG, "Switching to earpiece : HEAD ON");
                                    Log.d(TAG, "Old device = " + oldDevice);
                                }
                                earPieceActive = true;
                            } else if (oldDevice == DEVICE_OUT_EARPIECE) {
                                if (mVerboseLoggingEnabled) {
                                    Log.d(TAG, "Switching from earpiece : HEAD OFF");
                                    Log.d(TAG, "New device = " + device);
                                }
                                earPieceActive = false;
                            }

                            if (earPieceActive != mSarInfo.isEarPieceActive) {
                                mSarInfo.isEarPieceActive = earPieceActive;
                                updateSarScenario();
                            }
                        }
                    }
                }, filter, null, mHandler);
    }

    /**
     * Register the phone state listener.
     */
    private void registerPhoneStateListener() {
        Log.i(TAG, "Registering for telephony call state changes");
        mTelephonyManager.listen(
                mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    }

    /**
     * Update Wifi Client State
     */
    public void setClientWifiState(int state) {
        boolean newIsEnabled;
        if (state == WifiManager.WIFI_STATE_DISABLED) {
            newIsEnabled = false;
        } else if (state == WifiManager.WIFI_STATE_ENABLED) {
            newIsEnabled = true;
        } else {
            /* No change so exiting with no action */
            return;
        }

        /* Report change to HAL if needed */
        if (mSarInfo.isWifiClientEnabled != newIsEnabled) {
            mSarInfo.isWifiClientEnabled = newIsEnabled;
            updateSarScenario();
        }
    }

    /**
     * Update Wifi SoftAP State
     */
    public void setSapWifiState(int state) {
        boolean newIsEnabled;

        if (state == WifiManager.WIFI_AP_STATE_DISABLED) {
            newIsEnabled = false;
        } else if (state == WifiManager.WIFI_AP_STATE_ENABLED) {
            newIsEnabled = true;
        } else {
            /* No change so exiting with no action */
            return;
        }

        /* Report change to HAL if needed */
        if (mSarInfo.isWifiSapEnabled != newIsEnabled) {
            mSarInfo.isWifiSapEnabled = newIsEnabled;
            updateSarScenario();
        }
    }

    /**
     * Update Wifi ScanOnly State
     */
    public void setScanOnlyWifiState(int state) {
        boolean newIsEnabled;

        if (state == WifiManager.WIFI_STATE_DISABLED) {
            newIsEnabled = false;
        } else if (state == WifiManager.WIFI_STATE_ENABLED) {
            newIsEnabled = true;
        } else {
            /* No change so exiting with no action */
            return;
        }

        /* Report change to HAL if needed */
        if (mSarInfo.isWifiScanOnlyEnabled != newIsEnabled) {
            mSarInfo.isWifiScanOnlyEnabled = newIsEnabled;
            updateSarScenario();
        }
    }

    /**
     * Report Cell state event
     */
    private void onCellStateChangeEvent(int state) {
        boolean newIsVoiceCall;
        switch (state) {
            case CALL_STATE_OFFHOOK:
            case CALL_STATE_RINGING:
                newIsVoiceCall = true;
                break;

            case CALL_STATE_IDLE:
                newIsVoiceCall = false;
                break;

            default:
                Log.e(TAG, "Invalid Cell State: " + state);
                return;
        }

        /* Report change to HAL if needed */
        if (mSarInfo.isVoiceCall != newIsVoiceCall) {
            mSarInfo.isVoiceCall = newIsVoiceCall;

            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "Voice Call = " + newIsVoiceCall);
            }
            updateSarScenario();
        }
    }

    /**
     * Enable/disable verbose logging.
     */
    public void enableVerboseLogging(boolean verboseEnabled) {
        mVerboseLoggingEnabled = verboseEnabled;
    }

    /**
     * dump()
     * Dumps SarManager state (as well as its SarInfo member variable state)
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("Dump of SarManager");
        pw.println("isSarSupported: " + mSupportSarTxPowerLimit);
        pw.println("isSarVoiceCallSupported: " + mSupportSarVoiceCall);
        pw.println("isSarSoftApSupported: " + mSupportSarSoftAp);
        pw.println("");
        if (mSarInfo != null) {
            mSarInfo.dump(fd, pw, args);
        }
    }

    /**
     * Listen for phone call state events to set/reset TX power limits for SAR requirements.
     */
    private class WifiPhoneStateListener extends PhoneStateListener {
        WifiPhoneStateListener(Looper looper) {
            super(new HandlerExecutor(new Handler(looper)));
        }

        /**
         * onCallStateChanged()
         * This callback is called when a call state event is received
         * Note that this runs in the WifiCoreHandlerThread
         * since the corresponding Looper was passed to the WifiPhoneStateListener constructor.
         */
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            Log.d(TAG, "Received Phone State Change: " + state);

            /* In case of an unsolicited event */
            if (!mSupportSarTxPowerLimit || !mSupportSarVoiceCall) {
                return;
            }
            onCellStateChangeEvent(state);
        }
    }

    /**
     * updateSarScenario()
     * Update HAL with the new SAR scenario if needed.
     */
    private void updateSarScenario() {
        /* No action is taken if SAR is not supported */
        if (!mSupportSarTxPowerLimit) {
            return;
        }
        if (!mSarInfo.shouldReport()) {
            return;
        }

        /* Report info to HAL*/
        if (mWifiNative.selectTxPowerScenario(mSarInfo)) {
            mSarInfo.reportingSuccessful();
        } else {
            Log.e(TAG, "Failed in WifiNative.selectTxPowerScenario()");
        }

        return;
    }
}
