/*
 * Copyright (C) 2010, 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.connectivitymanagertest;

import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.os.SystemClock;
import android.test.InstrumentationTestCase;
import android.util.Log;
import android.view.KeyEvent;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.List;


/**
 * Base InstrumentationTestCase for Connectivity Manager (CM) test suite
 *
 * It registers connectivity manager broadcast and WiFi broadcast to provide
 * network connectivity information, also provides a set of utility functions
 * to modify and verify connectivity states.
 *
 * A CM test case should extend this base class.
 */
public class ConnectivityManagerTestBase extends InstrumentationTestCase {

    private static final String[] PING_HOST_LIST = {
        "www.google.com", "www.yahoo.com", "www.bing.com", "www.facebook.com", "www.ask.com"};

    protected static final int WAIT_FOR_SCAN_RESULT = 10 * 1000; //10 seconds
    protected static final int WIFI_SCAN_TIMEOUT = 50 * 1000; // 50 seconds
    protected static final int SHORT_TIMEOUT = 5 * 1000; // 5 seconds
    protected static final long LONG_TIMEOUT = 2 * 60 * 1000;  // 2 minutes
    protected static final long WIFI_CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
    // 2 minutes timer between wifi stop and start
    protected static final long  WIFI_STOP_START_INTERVAL = 2 * 60 * 1000; // 2 minutes
    // Set ping test timer to be 3 minutes
    protected static final long PING_TIMER = 3 * 60 *1000; // 3 minutes
    protected static final int SUCCESS = 0;  // for Wifi tethering state change
    protected static final int FAILURE = 1;
    protected static final int INIT = -1;

    protected final String mLogTag;

    private ConnectivityReceiver mConnectivityReceiver = null;
    private WifiReceiver mWifiReceiver = null;

    private long mLastConnectivityChangeTime = -1;
    protected ConnectivityManager mCm;
    private Context mContext;
    protected List<ScanResult> mLastScanResult;
    protected Object mWifiScanResultLock = new Object();

    /* Control Wifi States */
    public WifiManager mWifiManager;

    public ConnectivityManagerTestBase(String logTag) {
        super();
        mLogTag = logTag;
    }

    protected long getLastConnectivityChangeTime() {
        return mLastConnectivityChangeTime;
    }

    /**
     * A wrapper of a broadcast receiver which provides network connectivity information
     * for all kinds of network: wifi, mobile, etc.
     */
    private class ConnectivityReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            mLastConnectivityChangeTime = SystemClock.uptimeMillis();
            logv("ConnectivityReceiver: " + intent);
            String action = intent.getAction();
            if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                Log.v("ConnectivityReceiver", "onReceive() called with " + intent);
            }
        }
    }

    private class WifiReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.v("WifiReceiver", "onReceive() is calleld with " + intent);
            if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
                logv("scan results are available");
                synchronized (mWifiScanResultLock) {
                    mLastScanResult = mWifiManager.getScanResults();
                    mWifiScanResultLock.notifyAll();
                }
            }
        }
    }

    @Override
    protected void setUp() throws Exception {
        mLastScanResult = null;
        mContext = getInstrumentation().getContext();

        // Get an instance of ConnectivityManager
        mCm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
        // Get an instance of WifiManager
        mWifiManager =(WifiManager)mContext.getSystemService(Context.WIFI_SERVICE);

        if (mWifiManager.isWifiApEnabled()) {
            // if soft AP is enabled, disable it
            mWifiManager.setWifiApEnabled(null, false);
            logv("Disable soft ap");
        }

        // register a connectivity receiver for CONNECTIVITY_ACTION;
        mConnectivityReceiver = new ConnectivityReceiver();
        mContext.registerReceiver(mConnectivityReceiver,
                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        mWifiReceiver = new WifiReceiver();
        IntentFilter mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        mIntentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
        mIntentFilter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
        mIntentFilter.addAction(ConnectivityManager.ACTION_TETHER_STATE_CHANGED);
        mContext.registerReceiver(mWifiReceiver, mIntentFilter);

        logv("Clear Wifi before we start the test.");
        removeConfiguredNetworksAndDisableWifi();
     }

    // wait for network connectivity state: CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING,
    //                                      DISCONNECTED, UNKNOWN
    protected boolean waitForNetworkState(int networkType, State expectedState, long timeout) {
        long startTime = SystemClock.uptimeMillis();
        while (true) {
            NetworkInfo ni = mCm.getNetworkInfo(networkType);
            if (ni != null && expectedState.equals(ni.getState())) {
                logv("waitForNetworkState success: %s", ni);
                return true;
            }
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                logv("waitForNetworkState timeout: %s", ni);
                return false;
            }
            logv("waitForNetworkState interim: %s", ni);
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    // wait for Wifi state: WIFI_STATE_DISABLED, WIFI_STATE_DISABLING, WIFI_STATE_ENABLED,
    //                      WIFI_STATE_ENALBING, WIFI_STATE_UNKNOWN
    protected boolean waitForWifiState(int expectedState, long timeout) {
        long startTime = SystemClock.uptimeMillis();
        while (true) {
            int state = mWifiManager.getWifiState();
            if (state == expectedState) {
                logv("waitForWifiState success: state=" + state);
                return true;
            }
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                logv("waitForWifiState timeout: expected=%d, actual=%d", expectedState, state);
                return false;
            }
            logv("waitForWifiState interim: expected=%d, actual=%d", expectedState, state);
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    // Wait for Wifi AP state: WIFI_AP_STATE_DISABLED, WIFI_AP_STATE_DISABLING,
    //                         WIFI_AP_STATE_ENABLED, WIFI_STATE_ENALBING, WIFI_STATE_UNKNOWN
    protected boolean waitForWifiApState(int expectedState, long timeout) {
        long startTime = SystemClock.uptimeMillis();
        while (true) {
            int state = mWifiManager.getWifiApState();
            if (state == expectedState) {
                logv("waitForWifiAPState success: state=" + state);
                return true;
            }
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                logv(String.format("waitForWifiAPState timeout: expected=%d, actual=%d",
                        expectedState, state));
                return false;
            }
            logv(String.format("waitForWifiAPState interim: expected=%d, actual=%d",
                    expectedState, state));
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    /**
     * Wait for the wifi tethering result:
     * @param timeout is the maximum waiting time
     * @return SUCCESS if tethering result is successful
     *         FAILURE if tethering result returns error.
     */
    protected boolean waitForTetherStateChange(long timeout) {
        long startTime = SystemClock.uptimeMillis();
        String[] wifiRegexes = mCm.getTetherableWifiRegexs();
        while (true) {
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                return false;
            }
            String[] active = mCm.getTetheredIfaces();
            String[] error = mCm.getTetheringErroredIfaces();
            for (String iface: active) {
                for (String regex: wifiRegexes) {
                    if (iface.matches(regex)) {
                        return true;
                    }
                }
            }
            for (String iface: error) {
                for (String regex: wifiRegexes) {
                    if (iface.matches(regex)) {
                        return false;
                    }
                }
            }
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    // Return true if device is currently connected to mobile network
    protected boolean isConnectedToMobile() {
        return (mCm.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_MOBILE);
    }

    // Return true if device is currently connected to Wifi
    protected boolean isConnectedToWifi() {
        return (mCm.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI);
    }

    protected boolean enableWifi() {
        return mWifiManager.setWifiEnabled(true);
    }

    // Turn screen off
    protected void turnScreenOff() {
        logv("Turn screen off");
        PowerManager pm =
            (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        pm.goToSleep(SystemClock.uptimeMillis());
    }

    // Turn screen on
    protected void turnScreenOn() {
        logv("Turn screen on");
        PowerManager pm =
                (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
        pm.wakeUp(SystemClock.uptimeMillis());
        // disable lock screen
        KeyguardManager km = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
        if (km.inKeyguardRestrictedInputMode()) {
            sendKeys(KeyEvent.KEYCODE_MENU);
        }
    }

    /**
     * @return true if the ping test is successful, false otherwise.
     */
    protected boolean pingTest() {
        long startTime = System.currentTimeMillis();
        while ((System.currentTimeMillis() - startTime) < PING_TIMER) {
            try {
                // assume the chance that all servers are down is very small
                for (String host : PING_HOST_LIST) {
                    logv("Start ping test, ping " + host);
                    Process p = Runtime.getRuntime().exec("ping -c 10 -w 100 " + host);
                    int status = p.waitFor();
                    if (status == 0) {
                        // if any of the ping test is successful, return true
                        return true;
                    }
                }
            } catch (UnknownHostException e) {
                logv("Ping test Fail: Unknown Host");
            } catch (IOException e) {
                logv("Ping test Fail:  IOException");
            } catch (InterruptedException e) {
                logv("Ping test Fail: InterruptedException");
            }
            SystemClock.sleep(SHORT_TIMEOUT);
        }
        // ping test timeout
        return false;
    }

    /**
     * Associate the device to given SSID
     * If the device is already associated with a WiFi, disconnect and forget it,
     * We don't verify whether the connection is successful or not, leave this to the test
     */
    protected boolean connectToWifi(String ssid, String password) {
        WifiConfiguration config;
        if (password == null) {
            config = WifiConfigurationHelper.createOpenConfig(ssid);
        } else {
            config = WifiConfigurationHelper.createPskConfig(ssid, password);
        }
        return connectToWifiWithConfiguration(config);
    }

    /**
     * Connect to Wi-Fi with the given configuration. Note the SSID in the configuration
     * is pure string, we need to convert it to quoted string.
     */
    protected boolean connectToWifiWithConfiguration(WifiConfiguration config) {
        // If Wifi is not enabled, enable it
        if (!mWifiManager.isWifiEnabled()) {
            logv("Wifi is not enabled, enable it");
            mWifiManager.setWifiEnabled(true);
            // wait for the wifi state change before start scanning.
            if (!waitForWifiState(WifiManager.WIFI_STATE_ENABLED, LONG_TIMEOUT)) {
                logv("wait for WIFI_STATE_ENABLED failed");
                return false;
            }
        }

        // Save network configuration and connect to network without scanning
        mWifiManager.connect(config,
            new WifiManager.ActionListener() {
                @Override
                public void onSuccess() {
                }

                @Override
                public void onFailure(int reason) {
                    logv("connect failure " + reason);
                }
            });
        return true;
    }

    /*
     * Disconnect from the current AP and remove configured networks.
     */
    protected boolean disconnectAP() {
        // remove saved networks
        if (!mWifiManager.isWifiEnabled()) {
            logv("Enabled wifi before remove configured networks");
            mWifiManager.setWifiEnabled(true);
            SystemClock.sleep(SHORT_TIMEOUT);
        }

        List<WifiConfiguration> wifiConfigList = mWifiManager.getConfiguredNetworks();
        if (wifiConfigList == null) {
            logv("no configuration list is null");
            return true;
        }
        logv("size of wifiConfigList: " + wifiConfigList.size());
        for (WifiConfiguration wifiConfig: wifiConfigList) {
            logv("remove wifi configuration: " + wifiConfig.networkId);
            int netId = wifiConfig.networkId;
            mWifiManager.forget(netId, new WifiManager.ActionListener() {
                    @Override
                    public void onSuccess() {
                    }

                    @Override
                    public void onFailure(int reason) {
                        logv("Failed to forget " + reason);
                    }
                });
        }
        return true;
    }
    /**
     * Disable Wifi
     * @return true if Wifi is disabled successfully
     */
    protected boolean disableWifi() {
        return mWifiManager.setWifiEnabled(false);
    }

    /**
     * Remove configured networks and disable wifi
     */
    protected boolean removeConfiguredNetworksAndDisableWifi() {
        if (!disconnectAP()) {
           return false;
        }
        SystemClock.sleep(SHORT_TIMEOUT);
        if (!mWifiManager.setWifiEnabled(false)) {
            return false;
        }
        SystemClock.sleep(SHORT_TIMEOUT);
        return true;
    }

    protected static String convertToQuotedString(String string) {
        return "\"" + string + "\"";
    }

    protected boolean waitForActiveNetworkConnection(long timeout) {
        long startTime = SystemClock.uptimeMillis();
        while (true) {
            NetworkInfo ni = mCm.getActiveNetworkInfo();
            if (ni != null && ni.isConnected()) {
                return true;
            }
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                logv("waitForActiveNetworkConnection timeout: %s", ni);
                return false;
            }
            logv("waitForActiveNetworkConnection interim: %s", ni);
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    protected boolean waitUntilNoActiveNetworkConnection(long timeout) {
        long startTime = SystemClock.uptimeMillis();
        while (true) {
            NetworkInfo ni = mCm.getActiveNetworkInfo();
            if (ni == null) {
                return true;
            }
            if ((SystemClock.uptimeMillis() - startTime) > timeout) {
                logv("waitForActiveNetworkConnection timeout: %s", ni);
                return false;
            }
            logv("waitForActiveNetworkConnection interim: %s", ni);
            SystemClock.sleep(SHORT_TIMEOUT);
        }
    }

    // use ping request against Google public DNS to verify connectivity
    protected boolean checkNetworkConnectivity() {
        assertTrue("no active network connection", waitForActiveNetworkConnection(LONG_TIMEOUT));
        return pingTest();
    }

    @Override
    protected void tearDown() throws Exception{
        //Unregister receiver
        if (mConnectivityReceiver != null) {
          mContext.unregisterReceiver(mConnectivityReceiver);
        }
        if (mWifiReceiver != null) {
          mContext.unregisterReceiver(mWifiReceiver);
        }
        super.tearDown();
    }

    protected void logv(String format, Object... args) {
        Log.v(mLogTag, String.format(format, args));
    }

    /**
     * Connect to the provided Wi-Fi network
     * @param config is the network configuration
     * @throws AssertionError if fails to associate and connect to wifi ap
     */
    protected void connectToWifi(WifiConfiguration config) {
        // step 1: connect to the test access point
        assertTrue("failed to associate with " + config.SSID,
                connectToWifiWithConfiguration(config));

        // step 2: verify Wifi state and network state;
        assertTrue("wifi state not connected with " + config.SSID,
                waitForNetworkState(ConnectivityManager.TYPE_WIFI,
                State.CONNECTED, WIFI_CONNECTION_TIMEOUT));

        // step 3: verify the current connected network is the given SSID
        assertNotNull("no active wifi info", mWifiManager.getConnectionInfo());
        assertEquals("SSID mismatch", config.SSID, mWifiManager.getConnectionInfo().getSSID());
    }
}
