/*
 * Copyright 2017 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.hotspot2;

import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;

/**
 * Responsible for setup/monitor on Wi-Fi state and connection to the OSU AP.
 */
public class OsuNetworkConnection {
    private static final String TAG = "PasspointOsuNetworkConnection";
    private static final int TIMEOUT_MS = 10000;

    private final Context mContext;

    private boolean mVerboseLoggingEnabled = false;
    private WifiManager mWifiManager;
    private ConnectivityManager mConnectivityManager;
    private ConnectivityCallbacks mConnectivityCallbacks;
    private Callbacks mCallbacks;
    private Handler mHandler;
    private Network mNetwork = null;
    private boolean mConnected = false;
    private int mNetworkId = -1;
    private boolean mWifiEnabled = false;

    /**
     * Callbacks on Wi-Fi connection state changes.
     */
    public interface Callbacks {
        /**
         * Invoked when network connection is established with IP connectivity.
         *
         * @param network {@link Network} associated with the connected network.
         */
        void onConnected(Network network);

        /**
         * Invoked when the targeted network is disconnected.
         */
        void onDisconnected();

        /**
         * Invoked when a timer tracking connection request is not reset by successful connection.
         */
        void onTimeOut();

        /**
         * Invoked when Wifi is enabled.
         */
        void onWifiEnabled();

        /**
         * Invoked when Wifi is disabled.
         */
        void onWifiDisabled();
    }

    public OsuNetworkConnection(Context context) {
        mContext = context;
    }

    /**
     * Called to initialize tracking of wifi state and network events by registering for the
     * corresponding intents.
     */
    public void init(Handler handler) {
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
                    int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
                            WifiManager.WIFI_STATE_UNKNOWN);
                    if (state == WifiManager.WIFI_STATE_DISABLED && mWifiEnabled) {
                        mWifiEnabled = false;
                        if (mCallbacks != null) mCallbacks.onWifiDisabled();
                    }
                    if (state == WifiManager.WIFI_STATE_ENABLED && !mWifiEnabled) {
                        mWifiEnabled = true;
                        if (mCallbacks != null) mCallbacks.onWifiEnabled();
                    }
                }
            }
        };
        mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        mContext.registerReceiver(receiver, filter, null, handler);
        mWifiEnabled = mWifiManager.isWifiEnabled();
        mConnectivityManager =
                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
        mConnectivityCallbacks = new ConnectivityCallbacks();
        mHandler = handler;
    }

    /**
     * Disconnect, if required in the two cases
     * - still connected to the OSU AP
     * - connection to OSU AP was requested and in progress
     */
    public void disconnectIfNeeded() {
        if (mNetworkId < 0) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "No connection to tear down");
            }
            return;
        }
        mConnectivityManager.unregisterNetworkCallback(mConnectivityCallbacks);
        mWifiManager.removeNetwork(mNetworkId);
        mNetworkId = -1;
        mNetwork = null;
        mConnected = false;
    }

    /**
     * Register for network and Wifi state events
     *
     * @param callbacks The callbacks to be invoked on network change events
     */
    public void setEventCallback(Callbacks callbacks) {
        mCallbacks = callbacks;
    }

    /**
     * Connect to a OSU Wi-Fi network specified by the given SSID. The security type of the Wi-Fi
     * network is either open or OSEN (OSU Server-only authenticated layer 2 Encryption Network).
     * When network access identifier is provided, OSEN is used.
     *
     * @param ssid The SSID to connect to
     * @param nai Network access identifier of the network
     * @param friendlyName a friendly name of service provider
     *
     * @return boolean true if connection was successfully initiated
     */
    public boolean connect(WifiSsid ssid, String nai, String friendlyName) {
        if (mConnected) {
            if (mVerboseLoggingEnabled) {
                // Already connected
                Log.v(TAG, "Connect called twice");
            }
            return true;
        }
        if (!mWifiEnabled) {
            Log.w(TAG, "Wifi is not enabled");
            return false;
        }
        WifiConfiguration config = new WifiConfiguration();
        config.SSID = ssid.toString();

        // To suppress Wi-Fi has no internet access notification.
        config.noInternetAccessExpected = true;

        // To suppress Wi-Fi Sign-in notification for captive portal.
        config.osu = true;

        // Do not save this network
        config.ephemeral = true;
        config.providerFriendlyName = friendlyName;

        if (TextUtils.isEmpty(nai)) {
            config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
        } else {
            // Setup OSEN connection with Unauthenticated user TLS and WFA Root certs
            config.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OSEN);
            config.enterpriseConfig.setDomainSuffixMatch(nai);
            config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS);
            config.enterpriseConfig.setCaPath(WfaKeyStore.DEFAULT_WFA_CERT_DIR);
        }
        mNetworkId = mWifiManager.addNetwork(config);
        if (mNetworkId < 0) {
            Log.e(TAG, "Unable to add network");
            return false;
        }

        // NET_CAPABILITY_TRUSTED is added by builder by default.
        // But for ephemeral network, the capability needs to be removed
        // as wifi stack creates network agent without the capability.
        // That could cause connectivity service not to find the matching agent.
        NetworkRequest networkRequest = new NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .removeCapability(NET_CAPABILITY_TRUSTED)
                .build();
        mConnectivityManager.requestNetwork(networkRequest, mConnectivityCallbacks, mHandler,
                TIMEOUT_MS);

        // TODO(b/112195429): replace it with new connectivity API.
        if (!mWifiManager.enableNetwork(mNetworkId, true)) {
            Log.e(TAG, "Unable to enable network " + mNetworkId);
            disconnectIfNeeded();
            return false;
        }

        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "Current network ID " + mNetworkId);
        }
        return true;
    }

    /**
     * Method to update logging level in this class
     *
     * @param verbose enables verbose logging
     */
    public void enableVerboseLogging(boolean verbose) {
        mVerboseLoggingEnabled = verbose;
    }

    private class ConnectivityCallbacks extends ConnectivityManager.NetworkCallback {
        @Override
        public void onAvailable(Network network) {
            WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
            if (wifiInfo == null) {
                Log.w(TAG, "wifiInfo is not valid");
                return;
            }
            if (mNetworkId < 0 || mNetworkId != wifiInfo.getNetworkId()) {
                Log.w(TAG, "Irrelevant network available notification for netId: "
                        + wifiInfo.getNetworkId());
                return;
            }
            mNetwork = network;
            mConnected = true;
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "onLinkPropertiesChanged for network=" + network
                                + " isProvisioned?" + linkProperties.isProvisioned());
            }
            if (mNetwork == null) {
                Log.w(TAG, "ignore onLinkPropertyChanged event for null network");
                return;
            }
            if (linkProperties.isProvisioned()) {
                if (mCallbacks != null) {
                    mCallbacks.onConnected(network);
                }
            }
        }

        @Override
        public void onUnavailable() {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "onUnvailable ");
            }
            if (mCallbacks != null) {
                mCallbacks.onTimeOut();
            }
        }

        @Override
        public void onLost(Network network) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "onLost " + network);
            }
            if (network != mNetwork) {
                Log.w(TAG, "Irrelevant network lost notification");
                return;
            }
            if (mCallbacks != null) {
                mCallbacks.onDisconnected();
            }
        }
    }
}

