/*
 * 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.Activity;
import android.content.Context;
import android.content.BroadcastReceiver;
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.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration.KeyMgmt;
import android.os.Bundle;
import android.os.Handler;
import android.os.IPowerManager;
import android.os.Message;
import android.os.PowerManager;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.LinearLayout;

import com.android.internal.util.AsyncChannel;

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


/**
 * An activity registered with connectivity manager broadcast
 * provides network connectivity information and
 * can be used to set device states: Cellular, Wifi, Airplane mode.
 */
public class ConnectivityManagerTestActivity extends Activity {

    public static final String LOG_TAG = "ConnectivityManagerTestActivity";
    public static final int WAIT_FOR_SCAN_RESULT = 10 * 1000; //10 seconds
    public static final int WIFI_SCAN_TIMEOUT = 50 * 1000; // 50 seconds
    public static final int SHORT_TIMEOUT = 5 * 1000; // 5 seconds
    public static final long LONG_TIMEOUT = 50 * 1000;  // 50 seconds
    public static final long WIFI_CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes
    // 2 minutes timer between wifi stop and start
    public static final long  WIFI_STOP_START_INTERVAL = 2 * 60 * 1000; // 2 minutes
    // Set ping test timer to be 3 minutes
    public static final long PING_TIMER = 3 * 60 *1000; // 3 minutes
    public static final int SUCCESS = 0;  // for Wifi tethering state change
    public static final int FAILURE = 1;
    public static final int INIT = -1;
    private static final String ACCESS_POINT_FILE = "accesspoints.xml";
    public ConnectivityReceiver mConnectivityReceiver = null;
    public WifiReceiver mWifiReceiver = null;
    private AccessPointParserHelper mParseHelper = null;
    /*
     * Track network connectivity information
     */
    public State mState;
    public NetworkInfo mNetworkInfo;
    public NetworkInfo mOtherNetworkInfo;
    public boolean mIsFailOver;
    public String mReason;
    public boolean mScanResultIsAvailable = false;
    public ConnectivityManager mCM;
    public Object wifiObject = new Object();
    public Object connectivityObject = new Object();
    public int mWifiState;
    public NetworkInfo mWifiNetworkInfo;
    public String mBssid;
    public String mPowerSsid = "opennet"; //Default power SSID
    private Context mContext;
    public boolean scanResultAvailable = false;

    /*
     * Control Wifi States
     */
    public WifiManager mWifiManager;

    /*
     * Verify connectivity state
     */
    public static final int NUM_NETWORK_TYPES = ConnectivityManager.MAX_NETWORK_TYPE + 1;
    NetworkState[] connectivityState = new NetworkState[NUM_NETWORK_TYPES];

    // For wifi tethering tests
    private String[] mWifiRegexs;
    public int mWifiTetherResult = INIT;    // -1 is initialization state

    /**
     * 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) {
            log("ConnectivityReceiver: onReceive() is called with " + intent);
            String action = intent.getAction();
            if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                Log.v("ConnectivityReceiver", "onReceive() called with " + intent);
                return;
            }

            boolean noConnectivity =
                intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);

            if (noConnectivity) {
                mState = State.DISCONNECTED;
            } else {
                mState = State.CONNECTED;
            }

            mNetworkInfo = (NetworkInfo)
                intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);

            mOtherNetworkInfo = (NetworkInfo)
                intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);

            mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON);
            mIsFailOver = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);

            log("mNetworkInfo: " + mNetworkInfo.toString());
            if (mOtherNetworkInfo != null) {
                log("mOtherNetworkInfo: " + mOtherNetworkInfo.toString());
            }
            recordNetworkState(mNetworkInfo.getType(), mNetworkInfo.getState());
            if (mOtherNetworkInfo != null) {
                recordNetworkState(mOtherNetworkInfo.getType(), mOtherNetworkInfo.getState());
            }
            notifyNetworkConnectivityChange();
        }
    }

    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)) {
                log("scan results are available");
                notifyScanResult();
            } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                mWifiNetworkInfo =
                    (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                log("mWifiNetworkInfo: " + mWifiNetworkInfo.toString());
                if (mWifiNetworkInfo.getState() == State.CONNECTED) {
                    mBssid = intent.getStringExtra(WifiManager.EXTRA_BSSID);
                }
                notifyWifiState();
            } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
                mWifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
                                                WifiManager.WIFI_STATE_UNKNOWN);
                notifyWifiState();
            } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
                notifyWifiAPState();
            } else if (action.equals(ConnectivityManager.ACTION_TETHER_STATE_CHANGED)) {
                ArrayList<String> available = intent.getStringArrayListExtra(
                        ConnectivityManager.EXTRA_AVAILABLE_TETHER);
                ArrayList<String> active = intent.getStringArrayListExtra(
                        ConnectivityManager.EXTRA_ACTIVE_TETHER);
                ArrayList<String> errored = intent.getStringArrayListExtra(
                        ConnectivityManager.EXTRA_ERRORED_TETHER);
                updateTetherState(available.toArray(), active.toArray(), errored.toArray());
            }
            else {
                return;
            }
        }
    }

    private class WifiServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
                    if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
                        //AsyncChannel in msg.obj
                    } else {
                        log("Failed to establish AsyncChannel connection");
                    }
                    break;
                default:
                    //Ignore
                    break;
            }
        }
    }

    public ConnectivityManagerTestActivity() {
        mState = State.UNKNOWN;
        scanResultAvailable = false;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        log("onCreate, inst=" + Integer.toHexString(hashCode()));

        // Create a simple layout
        LinearLayout contentView = new LinearLayout(this);
        contentView.setOrientation(LinearLayout.VERTICAL);
        setContentView(contentView);
        setTitle("ConnectivityManagerTestActivity");


        // register a connectivity receiver for CONNECTIVITY_ACTION;
        mConnectivityReceiver = new ConnectivityReceiver();
        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);
        registerReceiver(mWifiReceiver, mIntentFilter);

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

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

        initializeNetworkStates();
        log("Clear Wifi before we start the test.");
        removeConfiguredNetworksAndDisableWifi();
        mWifiRegexs = mCM.getTetherableWifiRegexs();
     }

    public List<WifiConfiguration> loadNetworkConfigurations() throws Exception {
        InputStream in = getAssets().open(ACCESS_POINT_FILE);
        mParseHelper = new AccessPointParserHelper(in);
        return mParseHelper.getNetworkConfigurations();
    }

    // for each network type, initialize network states to UNKNOWN, and no verification flag is set
    public void initializeNetworkStates() {
        for (int networkType = NUM_NETWORK_TYPES - 1; networkType >=0; networkType--) {
            connectivityState[networkType] =  new NetworkState();
            log("Initialize network state for " + networkType + ": " +
                    connectivityState[networkType].toString());
        }
    }

    // deposit a network state
    public void recordNetworkState(int networkType, State networkState) {
        log("record network state for network " +  networkType +
                ", state is " + networkState);
        connectivityState[networkType].recordState(networkState);
    }

    // set the state transition criteria
    public void setStateTransitionCriteria(int networkType, State initState,
            int transitionDir, State targetState) {
        connectivityState[networkType].setStateTransitionCriteria(
                initState, transitionDir, targetState);
    }

    // Validate the states recorded
    public boolean validateNetworkStates(int networkType) {
        log("validate network state for " + networkType + ": ");
        return connectivityState[networkType].validateStateTransition();
    }

    // return result from network state validation
    public String getTransitionFailureReason(int networkType) {
        log("get network state transition failure reason for " + networkType + ": " +
                connectivityState[networkType].toString());
        return connectivityState[networkType].getReason();
    }

    private void notifyNetworkConnectivityChange() {
        synchronized(connectivityObject) {
            log("notify network connectivity changed");
            connectivityObject.notifyAll();
        }
    }
    private void notifyScanResult() {
        synchronized (this) {
            log("notify that scan results are available");
            scanResultAvailable = true;
            this.notify();
        }
    }

    private void notifyWifiState() {
        synchronized (wifiObject) {
            log("notify wifi state changed");
            wifiObject.notify();
        }
    }

    private void notifyWifiAPState() {
        synchronized (this) {
            log("notify wifi AP state changed");
            this.notify();
        }
    }

    // Update wifi tethering state
    private void updateTetherState(Object[] available, Object[] tethered, Object[] errored) {
        boolean wifiTethered = false;
        boolean wifiErrored = false;

        synchronized (this) {
            for (Object obj: tethered) {
                String str = (String)obj;
                for (String tethRex: mWifiRegexs) {
                    log("str: " + str +"tethRex: " + tethRex);
                    if (str.matches(tethRex)) {
                        wifiTethered = true;
                    }
                }
            }

            for (Object obj: errored) {
                String str = (String)obj;
                for (String tethRex: mWifiRegexs) {
                    log("error: str: " + str +"tethRex: " + tethRex);
                    if (str.matches(tethRex)) {
                        wifiErrored = true;
                    }
                }
            }

            if (wifiTethered) {
                mWifiTetherResult = SUCCESS;   // wifi tethering is successful
            } else if (wifiErrored) {
                mWifiTetherResult = FAILURE;   // wifi tethering failed
            }
            log("mWifiTetherResult: " + mWifiTetherResult);
            this.notify();
        }
    }


    // Wait for network connectivity state: CONNECTING, CONNECTED, SUSPENDED,
    //                                      DISCONNECTING, DISCONNECTED, UNKNOWN
    public boolean waitForNetworkState(int networkType, State expectedState, long timeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            if ((System.currentTimeMillis() - startTime) > timeout) {
                log("waitForNetworkState time out, the state of network type " + networkType +
                        " is: " + mCM.getNetworkInfo(networkType).getState());
                if (mCM.getNetworkInfo(networkType).getState() != expectedState) {
                    return false;
                } else {
                    // the broadcast has been sent out. the state has been changed.
                    log("networktype: " + networkType + " state: " +
                            mCM.getNetworkInfo(networkType));
                    return true;
                }
            }
            log("Wait for the connectivity state for network: " + networkType +
                    " to be " + expectedState.toString());
            synchronized (connectivityObject) {
                try {
                    connectivityObject.wait(SHORT_TIMEOUT);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if ((mNetworkInfo.getType() != networkType) ||
                    (mNetworkInfo.getState() != expectedState)) {
                    log("network state for " + mNetworkInfo.getType() +
                            "is: " + mNetworkInfo.getState());
                    continue;
                }
                return true;
            }
        }
    }

    // Wait for Wifi state: WIFI_STATE_DISABLED, WIFI_STATE_DISABLING, WIFI_STATE_ENABLED,
    //                      WIFI_STATE_ENALBING, WIFI_STATE_UNKNOWN
    public boolean waitForWifiState(int expectedState, long timeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            if ((System.currentTimeMillis() - startTime) > timeout) {
                if (mWifiState != expectedState) {
                    return false;
                } else {
                    return true;
                }
            }
            log("Wait for wifi state to be: " + expectedState);
            synchronized (wifiObject) {
                try {
                    wifiObject.wait(SHORT_TIMEOUT);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (mWifiState != expectedState) {
                    log("Wifi state is: " + mWifiState);
                    continue;
                }
                return true;
            }
        }
    }

    // Wait for Wifi AP state: WIFI_AP_STATE_DISABLED, WIFI_AP_STATE_DISABLING,
    //                         WIFI_AP_STATE_ENABLED, WIFI_STATE_ENALBING, WIFI_STATE_UNKNOWN
    public boolean waitForWifiAPState(int expectedState, long timeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            if ((System.currentTimeMillis() - startTime) > timeout) {
                if (mWifiManager.getWifiApState() != expectedState) {
                    return false;
                } else {
                    return true;
                }
            }
            log("Wait for wifi AP state to be: " + expectedState);
            synchronized (wifiObject) {
                try {
                    wifiObject.wait(SHORT_TIMEOUT);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (mWifiManager.getWifiApState() != expectedState) {
                    log("Wifi state is: " + mWifiManager.getWifiApState());
                    continue;
                }
                return true;
            }
        }
    }

    /**
     * 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.
     */
    public int waitForTetherStateChange(long timeout) {
        long startTime = System.currentTimeMillis();
        while (true) {
            if ((System.currentTimeMillis() - startTime) > timeout) {
                return mWifiTetherResult;
            }
            log("Wait for wifi tethering result.");
            synchronized (this) {
                try {
                    this.wait(SHORT_TIMEOUT);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (mWifiTetherResult == INIT ) {
                    continue;
                } else {
                    return mWifiTetherResult;
                }
            }
        }
    }

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

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

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

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

    // Turn screen on
    public void turnScreenOn() {
        log("Turn screen on");
        PowerManager pm =
                (PowerManager) getSystemService(Context.POWER_SERVICE);
        pm.wakeUp(SystemClock.uptimeMillis());
    }

    /**
     * @param pingServerList a list of servers that can be used for ping test, can be null
     * @return true if the ping test is successful, false otherwise.
     */
    public boolean pingTest(String[] pingServerList) {
        String[] hostList = {"www.google.com", "www.yahoo.com",
                "www.bing.com", "www.facebook.com", "www.ask.com"};
        if (pingServerList != null) {
            hostList = pingServerList;
        }

        long startTime = System.currentTimeMillis();
        while ((System.currentTimeMillis() - startTime) < PING_TIMER) {
            try {
                // assume the chance that all servers are down is very small
                for (int i = 0; i < hostList.length; i++ ) {
                    String host = hostList[i];
                    log("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) {
                log("Ping test Fail: Unknown Host");
            } catch (IOException e) {
                log("Ping test Fail:  IOException");
            } catch (InterruptedException e) {
                log("Ping test Fail: InterruptedException");
            }
        }
        // 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
     */
    public boolean connectToWifi(String knownSSID) {
        WifiConfiguration config = new WifiConfiguration();
        config.SSID = knownSSID;
        config.allowedKeyManagement.set(KeyMgmt.NONE);
        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.
     * @param config
     * @return
     */
    public boolean connectToWifiWithConfiguration(WifiConfiguration config) {
        String ssid = config.SSID;
        config.SSID = convertToQuotedString(ssid);

        // If Wifi is not enabled, enable it
        if (!mWifiManager.isWifiEnabled()) {
            log("Wifi is not enabled, enable it");
            mWifiManager.setWifiEnabled(true);
            // wait for the wifi state change before start scanning.
            if (!waitForWifiState(WifiManager.WIFI_STATE_ENABLED, 2*SHORT_TIMEOUT)) {
                log("wait for WIFI_STATE_ENABLED failed");
                return false;
            }
        }

        // Save network configuration and connect to network without scanning
        mWifiManager.connect(config,
            new WifiManager.ActionListener() {
                public void onSuccess() {
                }
                public void onFailure(int reason) {
                    log("connect failure " + reason);
                }
            });
        return true;
    }

    /*
     * Disconnect from the current AP and remove configured networks.
     */
    public boolean disconnectAP() {
        // remove saved networks
        if (!mWifiManager.isWifiEnabled()) {
            log("Enabled wifi before remove configured networks");
            mWifiManager.setWifiEnabled(true);
            sleep(SHORT_TIMEOUT);
        }
        List<WifiConfiguration> wifiConfigList = mWifiManager.getConfiguredNetworks();
        log("size of wifiConfigList: " + wifiConfigList.size());
        for (WifiConfiguration wifiConfig: wifiConfigList) {
            log("remove wifi configuration: " + wifiConfig.networkId);
            int netId = wifiConfig.networkId;
            mWifiManager.forget(netId, new WifiManager.ActionListener() {
                    public void onSuccess() {
                    }
                    public void onFailure(int reason) {
                        log("Failed to forget " + reason);
                    }
                });
        }
        return true;
    }
    /**
     * Disable Wifi
     * @return true if Wifi is disabled successfully
     */
    public boolean disableWifi() {
        return mWifiManager.setWifiEnabled(false);
    }

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

    private void sleep(long sleeptime) {
        try {
            Thread.sleep(sleeptime);
        } catch (InterruptedException e) {}
    }

    /**
     * Set airplane mode
     */
    public void setAirplaneMode(Context context, boolean enableAM) {
        //set the airplane mode
        Settings.Global.putInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON,
                enableAM ? 1 : 0);
        // Post the intent
        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        intent.putExtra("state", enableAM);
        context.sendBroadcastAsUser(intent, UserHandle.ALL);
    }

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

    @Override
    protected void onDestroy() {
        super.onDestroy();

        //Unregister receiver
        if (mConnectivityReceiver != null) {
            unregisterReceiver(mConnectivityReceiver);
        }
        if (mWifiReceiver != null) {
            unregisterReceiver(mWifiReceiver);
        }
        log("onDestroy, inst=" + Integer.toHexString(hashCode()));
    }

    @Override
    public void onStart() {
        super.onStart();
        mContext = this;
        Bundle bundle = this.getIntent().getExtras();
        if (bundle != null){
            mPowerSsid = bundle.getString("power_ssid");
        }
    }
    //A thread to set the device into airplane mode then turn on wifi.
    Thread setDeviceWifiAndAirplaneThread = new Thread(new Runnable() {
        public void run() {
            setAirplaneMode(mContext, true);
            connectToWifi(mPowerSsid);
        }
    });

    //A thread to set the device into wifi
    Thread setDeviceInWifiOnlyThread = new Thread(new Runnable() {
        public void run() {
            connectToWifi(mPowerSsid);
        }
    });

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            //This is a tricky way for the scripted monkey to
            //set the device in wifi and wifi in airplane mode.
            case KeyEvent.KEYCODE_1:
                setDeviceWifiAndAirplaneThread.start();
                break;

            case KeyEvent.KEYCODE_2:
                setDeviceInWifiOnlyThread.start();
                break;
        }
        return super.onKeyDown(keyCode, event);
    }

    private void log(String message) {
        Log.v(LOG_TAG, message);
    }
}
