/* * Copyright 2018 Myles McNamara * * 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 wifiwizard2; import org.apache.cordova.*; import java.util.List; import java.util.concurrent.Future; import java.lang.InterruptedException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.pm.PackageManager; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.IntentFilter; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.DhcpInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.ScanResult; import android.net.wifi.WifiInfo; import android.net.wifi.SupplicantState; import android.net.ConnectivityManager; import android.content.Context; import android.os.AsyncTask; import android.util.Log; import android.os.Build.VERSION; import java.net.URL; import java.net.InetAddress; import java.net.Inet4Address; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.HttpURLConnection; import java.net.UnknownHostException; public class WifiWizard2 extends CordovaPlugin { private static final String TAG = "WifiWizard2"; private static final int API_VERSION = VERSION.SDK_INT; private static final String ADD_NETWORK = "add"; private static final String REMOVE_NETWORK = "remove"; private static final String CONNECT_NETWORK = "connect"; private static final String DISCONNECT_NETWORK = "disconnectNetwork"; private static final String DISCONNECT = "disconnect"; private static final String LIST_NETWORKS = "listNetworks"; private static final String START_SCAN = "startScan"; private static final String GET_SCAN_RESULTS = "getScanResults"; private static final String GET_CONNECTED_SSID = "getConnectedSSID"; private static final String GET_CONNECTED_BSSID = "getConnectedBSSID"; private static final String GET_CONNECTED_NETWORKID = "getConnectedNetworkID"; private static final String IS_WIFI_ENABLED = "isWifiEnabled"; private static final String SET_WIFI_ENABLED = "setWifiEnabled"; private static final String SCAN = "scan"; private static final String ENABLE_NETWORK = "enable"; private static final String DISABLE_NETWORK = "disable"; private static final String GET_SSID_NET_ID = "getSSIDNetworkID"; private static final String REASSOCIATE = "reassociate"; private static final String RECONNECT = "reconnect"; private static final String REQUEST_FINE_LOCATION = "requestFineLocation"; private static final String GET_WIFI_IP_ADDRESS = "getWifiIP"; private static final String GET_WIFI_ROUTER_IP_ADDRESS = "getWifiRouterIP"; private static final String CAN_PING_WIFI_ROUTER = "canPingWifiRouter"; private static final String CAN_CONNECT_TO_ROUTER = "canConnectToRouter"; private static final String CAN_CONNECT_TO_INTERNET = "canConnectToInternet"; private static final String IS_CONNECTED_TO_INTERNET = "isConnectedToInternet"; private static final String RESET_BIND_ALL = "resetBindAll"; private static final String SET_BIND_ALL = "setBindAll"; private static final String GET_WIFI_IP_INFO = "getWifiIPInfo"; private static final int SCAN_RESULTS_CODE = 0; // Permissions request code for getScanResults() private static final int SCAN_CODE = 1; // Permissions request code for scan() private static final int LOCATION_REQUEST_CODE = 2; // Permissions request code private static final int WIFI_SERVICE_INFO_CODE = 3; private static final String ACCESS_FINE_LOCATION = android.Manifest.permission.ACCESS_FINE_LOCATION; private static int LAST_NET_ID = -1; // This is for when SSID or BSSID is requested but permissions have not been granted for location // we store whether or not BSSID was requested, to recall the getWifiServiceInfo fn after permissions are granted private static boolean bssidRequested = false; private WifiManager wifiManager; private CallbackContext callbackContext; private JSONArray passedData; private ConnectivityManager connectivityManager; private ConnectivityManager.NetworkCallback networkCallback; // Store AP, previous, and desired wifi info private AP previous, desired; private final BroadcastReceiver networkChangedReceiver = new NetworkChangedReceiver(); private static final IntentFilter NETWORK_STATE_CHANGED_FILTER = new IntentFilter(); static { NETWORK_STATE_CHANGED_FILTER.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); } /** * WEP has two kinds of password, a hex value that specifies the key or a character string used to * generate the real hex. This checks what kind of password has been supplied. The checks * correspond to WEP40, WEP104 & WEP232 */ private static boolean getHexKey(String s) { if (s == null) { return false; } int len = s.length(); if (len != 10 && len != 26 && len != 58) { return false; } for (int i = 0; i < len; ++i) { char c = s.charAt(i); if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return false; } } return true; } @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.wifiManager = (WifiManager) cordova.getActivity().getApplicationContext().getSystemService(Context.WIFI_SERVICE); this.connectivityManager = (ConnectivityManager) cordova.getActivity().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); } @Override public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException { this.callbackContext = callbackContext; this.passedData = data; // Actions that do not require WiFi to be enabled if (action.equals(IS_WIFI_ENABLED)) { this.isWifiEnabled(callbackContext); return true; } else if (action.equals(SET_WIFI_ENABLED)) { this.setWifiEnabled(callbackContext, data); return true; } else if (action.equals(REQUEST_FINE_LOCATION)) { this.requestLocationPermission(LOCATION_REQUEST_CODE); return true; } else if (action.equals(GET_WIFI_ROUTER_IP_ADDRESS)) { String ip = getWiFiRouterIP(); if ( ip == null || ip.equals("0.0.0.0")) { callbackContext.error("NO_VALID_ROUTER_IP_FOUND"); return true; } else { callbackContext.success(ip); return true; } } else if (action.equals(GET_WIFI_IP_ADDRESS) || action.equals(GET_WIFI_IP_INFO)) { String[] ipInfo = getWiFiIPAddress(); String ip = ipInfo[0]; String subnet = ipInfo[1]; if (ip == null || ip.equals("0.0.0.0")) { callbackContext.error("NO_VALID_IP_IDENTIFIED"); return true; } // Return only IP address if( action.equals( GET_WIFI_IP_ADDRESS ) ){ callbackContext.success(ip); return true; } // Return Wifi IP Info (subnet and IP as JSON object) JSONObject result = new JSONObject(); result.put("ip", ip); result.put("subnet", subnet); callbackContext.success(result); return true; } boolean wifiIsEnabled = verifyWifiEnabled(); if (!wifiIsEnabled) { callbackContext.error("WIFI_NOT_ENABLED"); return true; // Even though enable wifi failed, we still return true and handle error in callback } // Actions that DO require WiFi to be enabled if (action.equals(ADD_NETWORK)) { this.add(callbackContext, data); } else if (action.equals(IS_CONNECTED_TO_INTERNET)) { this.canConnectToInternet(callbackContext, true); } else if (action.equals(CAN_CONNECT_TO_INTERNET)) { this.canConnectToInternet(callbackContext, false); } else if (action.equals(CAN_PING_WIFI_ROUTER)) { this.canConnectToRouter(callbackContext, true); } else if (action.equals(CAN_CONNECT_TO_ROUTER)) { this.canConnectToRouter(callbackContext, false); } else if (action.equals(ENABLE_NETWORK)) { this.enable(callbackContext, data); } else if (action.equals(DISABLE_NETWORK)) { this.disable(callbackContext, data); } else if (action.equals(GET_SSID_NET_ID)) { this.getSSIDNetworkID(callbackContext, data); } else if (action.equals(REASSOCIATE)) { this.reassociate(callbackContext); } else if (action.equals(RECONNECT)) { this.reconnect(callbackContext); } else if (action.equals(SCAN)) { this.scan(callbackContext, data); } else if (action.equals(REMOVE_NETWORK)) { this.remove(callbackContext, data); } else if (action.equals(CONNECT_NETWORK)) { this.connect(callbackContext, data); } else if (action.equals(DISCONNECT_NETWORK)) { this.disconnectNetwork(callbackContext, data); } else if (action.equals(LIST_NETWORKS)) { this.listNetworks(callbackContext); } else if (action.equals(START_SCAN)) { this.startScan(callbackContext); } else if (action.equals(GET_SCAN_RESULTS)) { this.getScanResults(callbackContext, data); } else if (action.equals(DISCONNECT)) { this.disconnect(callbackContext); } else if (action.equals(GET_CONNECTED_SSID)) { this.getConnectedSSID(callbackContext); } else if (action.equals(GET_CONNECTED_BSSID)) { this.getConnectedBSSID(callbackContext); } else if (action.equals(GET_CONNECTED_NETWORKID)) { this.getConnectedNetworkID(callbackContext); } else if (action.equals(RESET_BIND_ALL)) { this.resetBindAll(callbackContext); } else if (action.equals(SET_BIND_ALL)) { this.setBindAll(callbackContext); } else { callbackContext.error("Incorrect action parameter: " + action); // The ONLY time to return FALSE is when action does not exist that was called // Returning false results in an INVALID_ACTION error, which translates to an error callback invoked on the JavaScript side // All other errors should be handled with the fail callback (callbackContext.error) // @see https://cordova.apache.org/docs/en/latest/guide/platforms/android/plugin.html return false; } return true; } /** * Scans networks and sends the list back on the success callback * * @param callbackContext A Cordova callback context * @param data JSONArray with [0] == JSONObject * @return true */ private boolean scan(final CallbackContext callbackContext, final JSONArray data) { Log.v(TAG, "Entering startScan"); final ScanSyncContext syncContext = new ScanSyncContext(); final BroadcastReceiver receiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { Log.v(TAG, "Entering onReceive"); synchronized (syncContext) { if (syncContext.finished) { Log.v(TAG, "In onReceive, already finished"); return; } syncContext.finished = true; context.unregisterReceiver(this); } Log.v(TAG, "In onReceive, success"); getScanResults(callbackContext, data); } }; final Context context = cordova.getActivity().getApplicationContext(); Log.v(TAG, "Submitting timeout to threadpool"); cordova.getThreadPool().submit(new Runnable() { public void run() { Log.v(TAG, "Entering timeout"); final int TEN_SECONDS = 10000; try { Thread.sleep(TEN_SECONDS); } catch (InterruptedException e) { Log.e(TAG, "Received InterruptedException e, " + e); // keep going into error } Log.v(TAG, "Thread sleep done"); synchronized (syncContext) { if (syncContext.finished) { Log.v(TAG, "In timeout, already finished"); return; } syncContext.finished = true; context.unregisterReceiver(receiver); } Log.v(TAG, "In timeout, error"); callbackContext.error("TIMEOUT_WAITING_FOR_SCAN"); } }); Log.v(TAG, "Registering broadcastReceiver"); context.registerReceiver( receiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) ); if (!wifiManager.startScan()) { Log.v(TAG, "Scan failed"); callbackContext.error("SCAN_FAILED"); return false; } Log.v(TAG, "Starting wifi scan"); return true; } /** * This methods adds a network to the list of available WiFi networks. If the network already * exists, then it updates it. * * @return true if add successful, false if add fails * @params callbackContext A Cordova callback context. * @params data JSON Array with [0] == SSID, [1] == password */ private boolean add(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: add entered."); // Initialize the WifiConfiguration object WifiConfiguration wifi = new WifiConfiguration(); try { // data's order for ANY object is // 0: SSID // 1: authentication algorithm, // 2: authentication information // 3: whether or not the SSID is hidden String newSSID = data.getString(0); String authType = data.getString(1); String newPass = data.getString(2); boolean isHiddenSSID = data.getBoolean(3); wifi.hiddenSSID = isHiddenSSID; if (authType.equals("WPA") || authType.equals("WPA2")) { /** * WPA Data format: * 0: ssid * 1: auth * 2: password * 3: isHiddenSSID */ wifi.SSID = newSSID; wifi.preSharedKey = newPass; wifi.status = WifiConfiguration.Status.ENABLED; wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); wifi.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); wifi.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); wifi.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); wifi.allowedProtocols.set(WifiConfiguration.Protocol.RSN); wifi.allowedProtocols.set(WifiConfiguration.Protocol.WPA); wifi.networkId = ssidToNetworkId(newSSID); } else if (authType.equals("WEP")) { /** * WEP Data format: * 0: ssid * 1: auth * 2: password * 3: isHiddenSSID */ wifi.SSID = newSSID; if (getHexKey(newPass)) { wifi.wepKeys[0] = newPass; } else { wifi.wepKeys[0] = "\"" + newPass + "\""; } wifi.wepTxKeyIndex = 0; wifi.status = WifiConfiguration.Status.ENABLED; wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40); wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104); wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP); wifi.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); wifi.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); wifi.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); wifi.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); wifi.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP); wifi.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); wifi.allowedProtocols.set(WifiConfiguration.Protocol.RSN); wifi.allowedProtocols.set(WifiConfiguration.Protocol.WPA); wifi.networkId = ssidToNetworkId(newSSID); } else if (authType.equals("NONE")) { /** * OPEN Network data format: * 0: ssid * 1: auth * 2: <not used> * 3: isHiddenSSID */ wifi.SSID = newSSID; wifi.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); wifi.networkId = ssidToNetworkId(newSSID); } else { Log.d(TAG, "Wifi Authentication Type Not Supported."); callbackContext.error("AUTH_TYPE_NOT_SUPPORTED"); return false; } // Set network to highest priority (deprecated in API >= 26) if( API_VERSION < 26 ){ wifi.priority = getMaxWifiPriority(wifiManager) + 1; } // After processing authentication types, add or update network if (wifi.networkId == -1) { // -1 means SSID configuration does not exist yet int newNetId = wifiManager.addNetwork(wifi); if( newNetId > -1 ){ callbackContext.success( newNetId ); } else { callbackContext.error( "ERROR_ADDING_NETWORK" ); } } else { int updatedNetID = wifiManager.updateNetwork(wifi); if( updatedNetID > -1 ){ callbackContext.success( updatedNetID ); } else { callbackContext.error( "ERROR_UPDATING_NETWORK" ); } } // WifiManager configurations are presistent for API 26+ if (API_VERSION < 26) { wifiManager.saveConfiguration(); // Call saveConfiguration for older < 26 API } return true; } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } } /** * This method connects a network. * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to connect */ private void enable(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: enable entered."); if (!validateData(data)) { callbackContext.error("ENABLE_INVALID_DATA"); Log.d(TAG, "WifiWizard2: enable invalid data."); return; } String ssidToEnable = ""; String bindAll = "false"; String waitForConnection = "false"; try { ssidToEnable = data.getString(0); bindAll = data.getString(1); waitForConnection = data.getString(2); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return; } int networkIdToEnable = ssidToNetworkId(ssidToEnable); try { if (networkIdToEnable > -1) { Log.d(TAG, "Valid networkIdToEnable: attempting connection"); // Bind all requests to WiFi network (only necessary for Lollipop+ - API 21+) if( bindAll.equals("true") ){ registerBindALL(networkIdToEnable); } if( wifiManager.enableNetwork(networkIdToEnable, true) ){ if( waitForConnection.equals("true") ){ callbackContext.success("NETWORK_ENABLED"); return; } else { new ConnectAsync().execute(callbackContext, networkIdToEnable); return; } } else { callbackContext.error("ERROR_ENABLING_NETWORK"); return; } } else { callbackContext.error("UNABLE_TO_ENABLE"); return; } } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return; } } /** * This method disables a network. * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to connect * @return true if network disconnected, false if failed */ private boolean disable(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: disable entered."); if (!validateData(data)) { callbackContext.error("DISABLE_INVALID_DATA"); Log.d(TAG, "WifiWizard2: disable invalid data"); return false; } String ssidToDisable = ""; try { ssidToDisable = data.getString(0); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } int networkIdToDisconnect = ssidToNetworkId(ssidToDisable); try { if (networkIdToDisconnect > 0) { if( wifiManager.disableNetwork(networkIdToDisconnect) ){ maybeResetBindALL(); callbackContext.success("Network " + ssidToDisable + " disabled!"); } else { callbackContext.error("UNABLE_TO_DISABLE"); } return true; } else { callbackContext.error("DISABLE_NETWORK_NOT_FOUND"); Log.d(TAG, "WifiWizard2: Network not found to disable."); return false; } } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } } /** * This method removes a network from the list of configured networks. * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to remove * @return true if network removed, false if failed */ private boolean remove(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: remove entered."); if (!validateData(data)) { callbackContext.error("REMOVE_INVALID_DATA"); Log.d(TAG, "WifiWizard2: remove data invalid"); return false; } // TODO: Verify the type of data! try { String ssidToDisconnect = data.getString(0); int networkIdToRemove = ssidToNetworkId(ssidToDisconnect); if (networkIdToRemove > -1) { if( wifiManager.removeNetwork(networkIdToRemove) ){ // Configurations persist by default in API 26+ if (API_VERSION < 26) { wifiManager.saveConfiguration(); } callbackContext.success("NETWORK_REMOVED"); } else { callbackContext.error( "UNABLE_TO_REMOVE" ); } return true; } else { callbackContext.error("REMOVE_NETWORK_NOT_FOUND"); Log.d(TAG, "WifiWizard2: Network not found, can't remove."); return false; } } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } } /** * This method connects a network. * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to connect */ private void connect(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: connect entered."); if (!validateData(data)) { callbackContext.error("CONNECT_INVALID_DATA"); Log.d(TAG, "WifiWizard2: connect invalid data."); return; } String ssidToConnect = ""; String bindAll = "false"; try { ssidToConnect = data.getString(0); bindAll = data.getString(1); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return; } int networkIdToConnect = ssidToNetworkId(ssidToConnect); if (networkIdToConnect > -1) { // We disable the network before connecting, because if this was the last connection before // a disconnect(), this will not reconnect. Log.d(TAG, "Valid networkIdToConnect: attempting connection"); // Bind all requests to WiFi network (only necessary for Lollipop+ - API 21+) if( bindAll.equals("true") ){ registerBindALL(networkIdToConnect); } if (API_VERSION >= 26) { // wifiManager.disconnect(); } else { wifiManager.disableNetwork(networkIdToConnect); } wifiManager.enableNetwork(networkIdToConnect, true); if (API_VERSION >= 26) { // wifiManager.reassociate(); } new ConnectAsync().execute(callbackContext, networkIdToConnect); return; } else { callbackContext.error("INVALID_NETWORK_ID_TO_CONNECT"); return; } } /** * Wait for connection before returning error or success * * This method will wait up to 60 seconds for WiFi connection to specified network ID be in COMPLETED state, otherwise will return error. * * @param callbackContext * @param networkIdToConnect * @return */ private class ConnectAsync extends AsyncTask<Object, Void, String[]> { CallbackContext callbackContext; @Override protected void onPostExecute(String[] results) { String error = results[0]; String success = results[1]; if (error != null) { this.callbackContext.error(error); } else { this.callbackContext.success(success); } } @Override protected String[] doInBackground(Object... params) { this.callbackContext = (CallbackContext) params[0]; int networkIdToConnect = (Integer) params[1]; final int TIMES_TO_RETRY = 15; for (int i = 0; i < TIMES_TO_RETRY; i++) { WifiInfo info = wifiManager.getConnectionInfo(); NetworkInfo.DetailedState connectionState = info .getDetailedStateOf(info.getSupplicantState()); boolean isConnected = // need to ensure we're on correct network because sometimes this code is // reached before the initial network has disconnected info.getNetworkId() == networkIdToConnect && ( connectionState == NetworkInfo.DetailedState.CONNECTED || // Android seems to sometimes get stuck in OBTAINING_IPADDR after it has received one (connectionState == NetworkInfo.DetailedState.OBTAINING_IPADDR && info.getIpAddress() != 0) ); if (isConnected) { return new String[]{ null, "NETWORK_CONNECTION_COMPLETED" }; } Log.d(TAG, "WifiWizard: Got " + connectionState.name() + " on " + (i + 1) + " out of " + TIMES_TO_RETRY); final int ONE_SECOND = 1000; try { Thread.sleep(ONE_SECOND); } catch (InterruptedException e) { Log.e(TAG, e.getMessage()); return new String[]{ "INTERRUPT_EXCEPT_WHILE_CONNECTING", null }; } } Log.d(TAG, "WifiWizard: Network failed to finish connecting within the timeout"); return new String[]{ "CONNECT_FAILED_TIMEOUT", null }; } } /** * This method disconnects a network. * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to connect * @return true if network disconnected, false if failed */ private boolean disconnectNetwork(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: disconnectNetwork entered."); if (!validateData(data)) { callbackContext.error("DISCONNECT_NET_INVALID_DATA"); Log.d(TAG, "WifiWizard2: disconnectNetwork invalid data"); return false; } String ssidToDisconnect = ""; // TODO: Verify type of data here! try { ssidToDisconnect = data.getString(0); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } int networkIdToDisconnect = ssidToNetworkId(ssidToDisconnect); if (networkIdToDisconnect > 0) { if( wifiManager.disableNetwork(networkIdToDisconnect) ){ maybeResetBindALL(); // We also remove the configuration from the device (use "disable" to keep config) if( wifiManager.removeNetwork(networkIdToDisconnect) ){ callbackContext.success("Network " + ssidToDisconnect + " disconnected and removed!"); } else { callbackContext.error("DISCONNECT_NET_REMOVE_ERROR"); Log.d(TAG, "WifiWizard2: Unable to remove network!"); return false; } } else { callbackContext.error("DISCONNECT_NET_DISABLE_ERROR"); Log.d(TAG, "WifiWizard2: Unable to disable network!"); return false; } return true; } else { callbackContext.error("DISCONNECT_NET_ID_NOT_FOUND"); Log.d(TAG, "WifiWizard2: Network not found to disconnect."); return false; } } /** * This method disconnects the currently connected network. * * @param callbackContext A Cordova callback context * @return true if network disconnected, false if failed */ private boolean disconnect(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: disconnect entered."); if (wifiManager.disconnect()) { maybeResetBindALL(); callbackContext.success("Disconnected from current network"); return true; } else { callbackContext.error("ERROR_DISCONNECT"); return false; } } /** * Reconnect Network * <p> * Reconnect to the currently active access point, if we are currently disconnected. This may * result in the asynchronous delivery of state change events. */ private boolean reconnect(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: reconnect entered."); if (wifiManager.reconnect()) { callbackContext.success("Reconnected network"); return true; } else { callbackContext.error("ERROR_RECONNECT"); return false; } } /** * Reassociate Network * <p> * Reconnect to the currently active access point, even if we are already connected. This may * result in the asynchronous delivery of state change events. */ private boolean reassociate(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: reassociate entered."); if (wifiManager.reassociate()) { callbackContext.success("Reassociated network"); return true; } else { callbackContext.error("ERROR_REASSOCIATE"); return false; } } /** * This method uses the callbackContext.success method to send a JSONArray of the currently * configured networks. * * @param callbackContext A Cordova callback context * @return true if network disconnected, false if failed */ private boolean listNetworks(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: listNetworks entered."); List<WifiConfiguration> wifiList = wifiManager.getConfiguredNetworks(); JSONArray returnList = new JSONArray(); for (WifiConfiguration wifi : wifiList) { returnList.put(wifi.SSID); } callbackContext.success(returnList); return true; } /** * This method uses the callbackContext.success method to send a JSONArray of the scanned * networks. * * @param callbackContext A Cordova callback context * @param data JSONArray with [0] == JSONObject * @return true */ private boolean getScanResults(CallbackContext callbackContext, JSONArray data) { if (cordova.hasPermission(ACCESS_FINE_LOCATION)) { List<ScanResult> scanResults = wifiManager.getScanResults(); JSONArray returnList = new JSONArray(); Integer numLevels = null; if (!validateData(data)) { callbackContext.error("GET_SCAN_RESULTS_INVALID_DATA"); Log.d(TAG, "WifiWizard2: getScanResults invalid data"); return false; } else if (!data.isNull(0)) { try { JSONObject options = data.getJSONObject(0); if (options.has("numLevels")) { Integer levels = options.optInt("numLevels"); if (levels > 0) { numLevels = levels; } else if (options.optBoolean("numLevels", false)) { // use previous default for {numLevels: true} numLevels = 5; } } } catch (JSONException e) { e.printStackTrace(); callbackContext.error(e.toString()); return false; } } for (ScanResult scan : scanResults) { /* * @todo - breaking change, remove this notice when tidying new release and explain changes, e.g.: * 0.y.z includes a breaking change to WifiWizard2.getScanResults(). * Earlier versions set scans' level attributes to a number derived from wifiManager.calculateSignalLevel. * This update returns scans' raw RSSI value as the level, per Android spec / APIs. * If your application depends on the previous behaviour, we have added an options object that will modify behaviour: * - if `(n == true || n < 2)`, `*.getScanResults({numLevels: n})` will return data as before, split in 5 levels; * - if `(n > 1)`, `*.getScanResults({numLevels: n})` will calculate the signal level, split in n levels; * - if `(n == false)`, `*.getScanResults({numLevels: n})` will use the raw signal level; */ int level; if (numLevels == null) { level = scan.level; } else { level = wifiManager.calculateSignalLevel(scan.level, numLevels); } JSONObject lvl = new JSONObject(); try { lvl.put("level", level); lvl.put("SSID", scan.SSID); lvl.put("BSSID", scan.BSSID); lvl.put("frequency", scan.frequency); lvl.put("capabilities", scan.capabilities); lvl.put("timestamp", scan.timestamp); if (API_VERSION >= 23) { // Marshmallow lvl.put("channelWidth", scan.channelWidth); lvl.put("centerFreq0", scan.centerFreq0); lvl.put("centerFreq1", scan.centerFreq1); } else { lvl.put("channelWidth", JSONObject.NULL); lvl.put("centerFreq0", JSONObject.NULL); lvl.put("centerFreq1", JSONObject.NULL); } returnList.put(lvl); } catch (JSONException e) { e.printStackTrace(); callbackContext.error(e.toString()); return false; } } callbackContext.success(returnList); return true; } else { requestLocationPermission(SCAN_RESULTS_CODE); return true; } } /** * This method uses the callbackContext.success method. It starts a wifi scanning * * @param callbackContext A Cordova callback context * @return true if started was successful */ private boolean startScan(CallbackContext callbackContext) { if (wifiManager.startScan()) { callbackContext.success(); return true; } else { callbackContext.error("STARTSCAN_FAILED"); return false; } } /** * This method returns the connected WiFi network ID (if connected) * * @return -1 if no network connected, or network id if connected */ private int getConnectedNetId() { int networkId = -1; WifiInfo info = wifiManager.getConnectionInfo(); if (info == null) { Log.d(TAG, "Unable to read wifi info"); return networkId; } networkId = info.getNetworkId(); if (networkId == -1) { Log.d(TAG, "NO_CURRENT_NETWORK_FOUND"); } return networkId; } /** * Get Network ID from SSID * * @param callbackContext A Cordova callback context * @param data JSON Array, with [0] being SSID to connect * @return true if network connected, false if failed */ private boolean getSSIDNetworkID(CallbackContext callbackContext, JSONArray data) { Log.d(TAG, "WifiWizard2: getSSIDNetworkID entered."); if (!validateData(data)) { callbackContext.error("GET_SSID_INVALID_DATA"); Log.d(TAG, "WifiWizard2: getSSIDNetworkID invalid data."); return false; } String ssidToGetNetworkID = ""; try { ssidToGetNetworkID = data.getString(0); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } int networkIdToConnect = ssidToNetworkId(ssidToGetNetworkID); callbackContext.success(networkIdToConnect); return true; } /** * This method returns the connected WiFi network ID (if connected) * * @param callbackContext A Cordova callback context * @return -1 if no network connected, or network id if connected */ private boolean getConnectedNetworkID(CallbackContext callbackContext) { int networkId = getConnectedNetId(); if (networkId == -1) { callbackContext.error("GET_CONNECTED_NET_ID_ERROR"); return false; } callbackContext.success(networkId); return true; } /** * This method retrieves the SSID for the currently connected network * * @param callbackContext A Cordova callback context * @return true if SSID found, false if not. */ private boolean getConnectedSSID(CallbackContext callbackContext) { return getWifiServiceInfo(callbackContext, false); } /** * This method retrieves the BSSID for the currently connected network * * @param callbackContext A Cordova callback context * @return true if SSID found, false if not. */ private boolean getConnectedBSSID(CallbackContext callbackContext) { return getWifiServiceInfo(callbackContext, true); } /** * This method retrieves the WifiInformation for the (SSID or BSSID) currently connected network. * * @param callbackContext A Cordova callback context * @param basicIdentifier A flag to get BSSID if true or SSID if false. * @return true if SSID found, false if not. */ private boolean getWifiServiceInfo(CallbackContext callbackContext, boolean basicIdentifier) { if (API_VERSION >= 23 && !cordova.hasPermission(ACCESS_FINE_LOCATION)) { //Android 9 (Pie) or newer requestLocationPermission(WIFI_SERVICE_INFO_CODE); bssidRequested = basicIdentifier; return true; } else { WifiInfo info = wifiManager.getConnectionInfo(); if (info == null) { callbackContext.error("UNABLE_TO_READ_WIFI_INFO"); return false; } // Only return SSID or BSSID when actually connected to a network SupplicantState state = info.getSupplicantState(); if (!state.equals(SupplicantState.COMPLETED)) { callbackContext.error("CONNECTION_NOT_COMPLETED"); return false; } String serviceInfo; if (basicIdentifier) { serviceInfo = info.getBSSID(); } else { serviceInfo = info.getSSID(); } if (serviceInfo == null || serviceInfo.isEmpty() || serviceInfo == "0x") { callbackContext.error("WIFI_INFORMATION_EMPTY"); return false; } // http://developer.android.com/reference/android/net/wifi/WifiInfo.html#getSSID() if (serviceInfo.startsWith("\"") && serviceInfo.endsWith("\"")) { serviceInfo = serviceInfo.substring(1, serviceInfo.length() - 1); } callbackContext.success(serviceInfo); return true; } } /** * This method retrieves the current WiFi status * * @param callbackContext A Cordova callback context * @return true if WiFi is enabled, fail will be called if not. */ private boolean isWifiEnabled(CallbackContext callbackContext) { boolean isEnabled = wifiManager.isWifiEnabled(); callbackContext.success(isEnabled ? "1" : "0"); return isEnabled; } /** * This method takes a given String, searches the current list of configured WiFi networks, and * returns the networkId for the network if the SSID matches. If not, it returns -1. */ private int ssidToNetworkId(String ssid) { try { int maybeNetId = Integer.parseInt(ssid); Log.d(TAG, "ssidToNetworkId passed SSID is integer, probably a Network ID: " + ssid); return maybeNetId; } catch (NumberFormatException e) { List<WifiConfiguration> currentNetworks = wifiManager.getConfiguredNetworks(); int networkId = -1; // For each network in the list, compare the SSID with the given one for (WifiConfiguration test : currentNetworks) { if (test.SSID != null && test.SSID.equals(ssid)) { networkId = test.networkId; } } return networkId; } } /** * This method enables or disables the wifi */ private boolean setWifiEnabled(CallbackContext callbackContext, JSONArray data) { if (!validateData(data)) { callbackContext.error("SETWIFIENABLED_INVALID_DATA"); Log.d(TAG, "WifiWizard2: setWifiEnabled invalid data"); return false; } String status = ""; try { status = data.getString(0); } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } if (wifiManager.setWifiEnabled(status.equals("true"))) { callbackContext.success(); return true; } else { callbackContext.error("ERROR_SETWIFIENABLED"); return false; } } /** * This method will check if WiFi is enabled, and enable it if not, waiting up to 10 seconds for * it to enable * * @return True if wifi is enabled, false if unable to enable wifi */ private boolean verifyWifiEnabled() { Log.d(TAG, "WifiWizard2: verifyWifiEnabled entered."); if (!wifiManager.isWifiEnabled()) { Log.i(TAG, "Enabling wi-fi..."); if (wifiManager.setWifiEnabled(true)) { Log.i(TAG, "Wi-fi enabled"); } else { Log.e(TAG, "VERIFY_ERROR_ENABLE_WIFI"); return false; } // This happens very quickly, but need to wait for it to enable. A little busy wait? int count = 0; while (!wifiManager.isWifiEnabled()) { if (count >= 10) { Log.i(TAG, "Took too long to enable wi-fi, quitting"); return false; } Log.i(TAG, "Still waiting for wi-fi to enable..."); try { Thread.sleep(1000L); } catch (InterruptedException ie) { // continue } count++; } // If we make it this far, wifi should be enabled by now return true; } else { return true; } } /** * Format and return WiFi IPv4 Address * @return */ private String[] getWiFiIPAddress() { WifiInfo wifiInfo = wifiManager.getConnectionInfo(); int ip = wifiInfo.getIpAddress(); String ipString = formatIP(ip); String subnet = ""; try { InetAddress inetAddress = InetAddress.getByName(ipString); subnet = getIPv4Subnet(inetAddress); } catch (Exception e) { } return new String[]{ipString, subnet}; } /** * Get WiFi Router IP from DHCP * @return */ private String getWiFiRouterIP() { DhcpInfo dhcp = wifiManager.getDhcpInfo(); int ip = dhcp.gateway; return formatIP(ip); } /** * Format IPv4 Address * @param ip * @return */ private String formatIP(int ip) { return String.format( "%d.%d.%d.%d", (ip & 0xff), (ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff) ); } /** * Get IPv4 Subnet * @param inetAddress * @return */ public static String getIPv4Subnet(InetAddress inetAddress) { try { NetworkInterface ni = NetworkInterface.getByInetAddress(inetAddress); List<InterfaceAddress> intAddrs = ni.getInterfaceAddresses(); for (InterfaceAddress ia : intAddrs) { if (!ia.getAddress().isLoopbackAddress() && ia.getAddress() instanceof Inet4Address) { return getIPv4SubnetFromNetPrefixLength(ia.getNetworkPrefixLength()).getHostAddress() .toString(); } } } catch (Exception e) { } return ""; } /** * Get Subnet from Prefix Length * @param netPrefixLength * @return */ public static InetAddress getIPv4SubnetFromNetPrefixLength(int netPrefixLength) { try { int shift = (1 << 31); for (int i = netPrefixLength - 1; i > 0; i--) { shift = (shift >> 1); } String subnet = Integer.toString((shift >> 24) & 255) + "." + Integer.toString((shift >> 16) & 255) + "." + Integer.toString((shift >> 8) & 255) + "." + Integer.toString(shift & 255); return InetAddress.getByName(subnet); } catch (Exception e) { } return null; } /** * Validate JSON data */ private boolean validateData(JSONArray data) { try { if (data == null || data.get(0) == null) { callbackContext.error("DATA_IS_NULL"); return false; } return true; } catch (Exception e) { callbackContext.error(e.getMessage()); } return false; } /** * Request ACCESS_FINE_LOCATION Permission * @param requestCode */ protected void requestLocationPermission(int requestCode) { cordova.requestPermission(this, requestCode, ACCESS_FINE_LOCATION); } /** * Handle Android Permission Requests */ public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { for (int r : grantResults) { if (r == PackageManager.PERMISSION_DENIED) { callbackContext.error( "PERMISSION_DENIED" ); return; } } switch (requestCode) { case SCAN_RESULTS_CODE: getScanResults(callbackContext, passedData); // Call method again after permissions approved break; case SCAN_CODE: scan(callbackContext, passedData); // Call method again after permissions approved break; case LOCATION_REQUEST_CODE: callbackContext.success("PERMISSION_GRANTED"); break; case WIFI_SERVICE_INFO_CODE: getWifiServiceInfo(callbackContext, bssidRequested); break; } } /** * Figure out what the highest priority network in the network list is and return that priority */ private static int getMaxWifiPriority(final WifiManager wifiManager) { final List<WifiConfiguration> configurations = wifiManager.getConfiguredNetworks(); int maxPriority = 0; for (WifiConfiguration config : configurations) { if (config.priority > maxPriority) { maxPriority = config.priority; } } Log.d(TAG, "WifiWizard: Found max WiFi priority of " + maxPriority); return maxPriority; } /** * Check if device is connected to Internet */ private boolean canConnectToInternet(CallbackContext callbackContext, boolean doPing) { try { if ( hasInternetConnection(doPing) ) { // Send success as 1 to return true from Promise (handled in JS) callbackContext.success("1"); return true; } else { callbackContext.success("0"); return false; } } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } } /** * Check if we can conenct to router via HTTP connection * * @param callbackContext * @param doPing * @return boolean */ private boolean canConnectToRouter(CallbackContext callbackContext, boolean doPing) { try { if (hasConnectionToRouter(doPing)) { // Send success as 1 to return true from Promise (handled in JS) callbackContext.success("1"); return true; } else { callbackContext.success("0"); return false; } } catch (Exception e) { callbackContext.error(e.getMessage()); Log.d(TAG, e.getMessage()); return false; } } /** * Check if The Device Is Connected to Internet * * @return true if device connect to Internet or return false if not */ public boolean hasInternetConnection(boolean doPing) { if (connectivityManager != null) { NetworkInfo info = connectivityManager.getActiveNetworkInfo(); if (info != null) { if (info.isConnected()) { if( doPing ){ return pingCmd("8.8.8.8"); } else { return isHTTPreachable("http://www.google.com/"); } } } } return false; } /** * Check for connection to router by pinging router IP * @return */ public boolean hasConnectionToRouter( boolean doPing ) { String ip = getWiFiRouterIP(); if ( ip == null || ip.equals("0.0.0.0") || connectivityManager == null) { return false; } else { NetworkInfo info = connectivityManager.getActiveNetworkInfo(); if (info != null && info.isConnected()) { if( doPing ){ return pingCmd(ip); } else { return isHTTPreachable("http://" + ip + "/"); } } else { return false; } } } /** * Check if HTTP connection to URL is reachable * * @param checkURL * @return boolean */ public static boolean isHTTPreachable(String checkURL) { try { // make a URL to a known source URL url = new URL(checkURL); // open a connection to that source HttpURLConnection urlConnect = (HttpURLConnection) url.openConnection(); // trying to retrieve data from the source. If there // is no connection, this line will fail Object objData = urlConnect.getContent(); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * Method to Ping IP Address * * @param addr IP address you want to ping it * @return true if the IP address is reachable */ public boolean pingCmd(String addr) { try { String ping = "ping -c 1 -W 3 " + addr; Runtime run = Runtime.getRuntime(); Process pro = run.exec(ping); try { pro.waitFor(); } catch (InterruptedException e) { Log.e(TAG, "InterruptedException error.", e); } int exit = pro.exitValue(); Log.d(TAG, "pingCmd exitValue" + exit); if (exit == 0) { return true; } else { // ip address is not reachable return false; } } catch (UnknownHostException e) { Log.d(TAG, "UnknownHostException: " + e.getMessage()); } catch (Exception e) { Log.d(TAG, e.getMessage()); } return false; } /** * Network Changed Broadcast Receiver */ private class NetworkChangedReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, final Intent intent) { if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) { Log.d(TAG, "NETWORK_STATE_CHANGED_ACTION"); NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); WifiInfo info = WifiWizard2.this.wifiManager.getConnectionInfo(); // Checks that you're connected to the desired network if (networkInfo.isConnected() && info.getNetworkId() > -1) { final String ssid = info.getSSID().replaceAll("\"", ""); final String bssid = info.getBSSID(); Log.d(TAG, "Connected to '" + ssid + "' @ " + bssid); // Verify the desired network ID is what we actually connected to if ( desired != null && info.getNetworkId() == desired.apId ) { onSuccessfulConnection(); } } } } } /** * Register Receiver for Network Changed to handle BindALL * @param netID */ private void registerBindALL(int netID){ // Bind all requests to WiFi network (only necessary for Lollipop+ - API 21+) if( API_VERSION > 21 ){ Log.d(TAG, "registerBindALL: registering net changed receiver"); desired = new AP(netID,null,null); cordova.getActivity().getApplicationContext().registerReceiver(networkChangedReceiver, NETWORK_STATE_CHANGED_FILTER); } else { Log.d(TAG, "registerBindALL: API older than 21, bindall ignored."); } } /** * Maybe reset bind all after disconnect/disable * * This method unregisters the network changed receiver, as well as setting null for * bindProcessToNetwork or setProcessDefaultNetwork to prevent future sockets from application * being routed through Wifi. */ private void maybeResetBindALL(){ Log.d(TAG, "maybeResetBindALL"); // desired should have a value if receiver is registered if( desired != null ){ if( API_VERSION > 21 ){ try { // Unregister net changed receiver -- should only be registered in API versions > 21 cordova.getActivity().getApplicationContext().unregisterReceiver(networkChangedReceiver); } catch (Exception e) {} } // Lollipop OS or newer if ( API_VERSION >= 23 ) { connectivityManager.bindProcessToNetwork(null); } else if( API_VERSION >= 21 && API_VERSION < 23 ){ connectivityManager.setProcessDefaultNetwork(null); } if ( API_VERSION > 21 && networkCallback != null) { try { // Same behavior as releaseNetworkRequest connectivityManager.unregisterNetworkCallback(networkCallback); // Added in API 21 } catch (Exception e) {} } networkCallback = null; previous = null; desired = null; } } /** * Will un-bind to network (use Cellular network) * * @param callbackContext A Cordova callback context */ private void resetBindAll(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: resetBindALL"); try { maybeResetBindALL(); callbackContext.success("Successfully reset BindALL"); } catch (Exception e) { Log.e(TAG, "InterruptedException error.", e); callbackContext.error("ERROR_NO_BIND_ALL"); } } /** * Will bind to network (use Wifi network) * * @param callbackContext A Cordova callback context */ private void setBindAll(CallbackContext callbackContext) { Log.d(TAG, "WifiWizard2: setBindALL"); try { int networkId = getConnectedNetId(); registerBindALL(networkId); callbackContext.success("Successfully bindAll to network"); } catch (Exception e) { Log.e(TAG, "InterruptedException error.", e); callbackContext.error("ERROR_CANT_BIND_ALL"); } } /** * Called after successful connection to WiFi when using BindAll feature * * This method is called by the NetworkChangedReceiver after network changed action, and confirming that we are in fact connected to wifi, * and the wifi we're connected to, is the correct network set in enable, or connect. */ private void onSuccessfulConnection() { // On Lollipop+ the OS routes network requests through mobile data // when phone is attached to a wifi that doesn't have Internet connection // We use the ConnectivityManager to force bind all requests from our process // to the wifi without internet // see https://android-developers.googleblog.com/2016/07/connecting-your-app-to-wi-fi-device.html // Marshmallow OS or newer if ( API_VERSION >= 23 ) { Log.d(TAG, "BindALL onSuccessfulConnection API >= 23"); // Marshmallow (API 23+) or newer uses bindProcessToNetwork final NetworkRequest request = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) // .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build(); networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { if( connectivityManager.bindProcessToNetwork(network) ){ Log.d(TAG, "bindProcessToNetwork TRUE onSuccessfulConnection"); } else { Log.d(TAG, "bindProcessToNetwork FALSE onSuccessfulConnection"); } } }; connectivityManager.requestNetwork(request, networkCallback); // Only lollipop (API 21 && 22) use setProcessDefaultNetwork, API < 21 already does this by default } else if( API_VERSION >= 21 && API_VERSION < 23 ){ Log.d(TAG, "BindALL onSuccessfulConnection API >= 21 && < 23"); // Lollipop (API 21-22) use setProcessDefaultNetwork (deprecated in API 23 - Marshmallow) final NetworkRequest request = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) // .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build(); networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { connectivityManager.setProcessDefaultNetwork(network); } }; connectivityManager.requestNetwork(request, networkCallback); } else { // Technically we should never reach this with older API, but just in case Log.d(TAG, "BindALL onSuccessfulConnection API older than 21, no need to do any binding"); networkCallback = null; previous = null; desired = null; } } /** * Class to store finished boolean in */ private class ScanSyncContext { public boolean finished = false; } /** * Used for storing access point information */ private static class AP { final String ssid, bssid; final int apId; AP(int apId, final String ssid, final String bssid) { this.apId = apId; this.ssid = ssid; this.bssid = bssid; } } }