TaifunWiFi 拓展:手机WiFi连接管理

« 返回首页

TaifunWiFi 拓展:手机WiFi连接管理

  • .aix 拓展下载:

com.puravidaapps.TaifunWiFi.aix

  • demo程序下载:

wifi.aia

一些在无线局域网中使用的有用块。

所需权限:

android.permission.ACCESS_WIFI_STATEandroid.permission.CHANGE_WIFI_STATEandroid.permission.ACCESS_NETWORK_STATEandroid.permission.ACCESS_FINE_LOCATIONandroid.permission.WRITE_SETTINGS

App截图:

测试代码块:

拓展源码 TaifunWiFi.java

//-*- mode: java; c-basic-offset: 2; -*-
package com.puravidaapps;
// Version 2: form.registerForDestroy(this) added, MacAddress added
// Version 2a: avoid DX execution failed error: build each extension separately
// Version 3: BSSID
// Version 3a: bugfix "IllegalArgumentException: Receiver not registered: null" while switching screens
// Version 3b: bugfix "IllegalArgumentException: Receiver not registered" while switching screens
// Version 4: correspondingRSSIs, SignalStrength, ConnectionInfo, Is5GHzBandSupported added
// Version 5: correspondingBSSIs added
// Version 5a: fix ConnectSSID method, see also https://groups.google.com/d/msg/mitappinventortest/BvbzgUeHMGs/rw1-V60UBgAJ
// Version 5b: fix ConnectSSID method, see also https://groups.google.com/d/msg/mitappinventortest/BvbzgUeHMGs/ErFcSgnWBQAJ
// Version 6: new MacAddress workaround, new AccessPointIP method
// Version 7: connect without password
// Version 8: Disconnect method added
// Version 9: DNS method added
// Version 10: SDK 26 update: remove dangerous permission android.permission.ACCESS_COARSE_LOCATION, AvailableSSIDs removed
// Version 11: AvailableSSIDs added again. Additionally example project provided about how to use it together with the location sensor
// Version 12: AfterWifiNegotiation event added
// Version 13: RemoveSSID added
// Version 14: LocalIP: returns wifi ip if its enabled else the cellular one
// Version 15: android.permission.ACCESS_NETWORK_STATE added for App Inventor nb188 release

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.RouteInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.SupplicantState;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.http.conn.util.InetAddressUtils;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.runtime.*;
import com.google.appinventor.components.runtime.util.AsynchUtil;



@SuppressWarnings("deprecation")
@DesignerComponent(version = TaifunWiFi.VERSION,
    description = "WiFi Manager Extension. Version 15 as of 2021-11-12.",
    category = ComponentCategory.EXTENSION,
    nonVisible = true,
    iconName = "https://puravidaapps.com/images/taifun16.png",
    helpUrl = "https://puravidaapps.com/wifi.php")
@SimpleObject(external = true)
@UsesPermissions(permissionNames = "android.permission.ACCESS_WIFI_STATE, android.permission.CHANGE_WIFI_STATE, android.permission.ACCESS_NETWORK_STATE")

public class TaifunWiFi extends AndroidNonvisibleComponent implements Component, OnDestroyListener {

  public static final int VERSION = 14;
  private ComponentContainer container;
  private Context context;
  private final Activity activity;
  private static final String LOG_TAG = "TaifunWiFi";
  private boolean suppressSuccessMessage;
  private boolean suppressWarnings;
  private static WifiManager wm;
  private boolean isRepl = false;
  private WiFiReceiverScan wiFiReceiverScan; 


  public TaifunWiFi(ComponentContainer container) {
    super(container.$form());
    if (form instanceof ReplForm) { // Note: form is defined in our superclass
      isRepl = true;
    }
    this.container = container;
    context = (Context) container.$context();
    activity = (Activity) container.$context();
    wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
   Log.d(LOG_TAG, "TaifunWiFi Created" );
  }

  /**
   * Returns whether Success Message should be suppressed
   */
  @SimpleProperty(category = PropertyCategory.BEHAVIOR,
      description = "whether Success Message should be suppressed. Used in Enable and Disable method.")
  public boolean SuppressSuccessMessage() {
    return suppressSuccessMessage;
  }


  /**
   * Specifies whether Success Message should be suppressed
   */
  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN,
      defaultValue = "false")
  @SimpleProperty
  public void SuppressSuccessMessage(boolean suppressSuccessMessage) {
    this.suppressSuccessMessage = suppressSuccessMessage;
  }


  
  /**
   * Returns whether Warnings should be suppressed
   */
  @SimpleProperty(category = PropertyCategory.BEHAVIOR,
      description = "whether Warnings should be suppressed")
  public boolean SuppressWarnings() {
    return suppressWarnings;
  }


  /**
   * Specifies whether Warnings should be suppressed
   */
  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN,
      defaultValue = "false")
  @SimpleProperty
  public void SuppressWarnings(boolean suppressWarnings) {
    this.suppressWarnings = suppressWarnings;
  }
  
  
  @SimpleFunction(description = "Return the local IP Address. Returns wifi ip if its enabled else the cellular one.")
  public String LocalIP() {
    if(wm.isWifiEnabled()) {
      // http://stackoverflow.com/a/18436895/1545993
      final WifiInfo wi = wm.getConnectionInfo();
      int ipAddress = wi.getIpAddress();
      String ip = String.format("%d.%d.%d.%d", (ipAddress & 0xff),(ipAddress >> 8 & 0xff),(ipAddress >> 16 & 0xff),(ipAddress >> 24 & 0xff));
      return ip;
    }

    // 2019-10-21
    // https://stackoverflow.com/a/30183130
    try {
      for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
        NetworkInterface intf = en.nextElement();
        for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
          InetAddress inetAddress = enumIpAddr.nextElement();
          Log.i(LOG_TAG, "inetAddress.getHostAddress(): "+ inetAddress.getHostAddress());
          //the condition after && is missing in your snippet, checking instance of inetAddress
          if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
            Log.i(LOG_TAG, "return inetAddress.getHostAddress(): "+ inetAddress.getHostAddress());
            return inetAddress.getHostAddress();
          }
        }
      }
    } catch (Exception e) {
      return "";
    }     
    return "";
  }

  
  /**
   * http://www.programcreek.com/java-api-examples/android.net.wifi.WifiManager 
   */
  @SimpleFunction(description = "Get current WiFi state: true or false.")
  public boolean IsEnabled(){
    final WifiInfo wi = wm.getConnectionInfo();
    try {
      if (!wm.isWifiEnabled() || wi.getSSID() == null) {
        return false;
      } else {
        return true;
      }
    } catch (Exception e) {
      return false;
    } 
  }


  /**
   * http://www.programcreek.com/java-api-examples/android.net.wifi.WifiManager 
   */
  @SimpleFunction(description = "Enable WiFi. You can hide the success message after setting the suppressSuccessMessage property to false.")
  public void Enable(){
    if (isRepl){
      Log.w(LOG_TAG, "You have to build the app to be able to use this method!");
      if (!suppressWarnings) {
        Toast.makeText(context, "You have to build the app to be able to use this method!", Toast.LENGTH_SHORT).show();
      }  
    } else {  
      wm.setWifiEnabled(true);
      if (!suppressSuccessMessage) {
        Toast.makeText(context, "WiFi enabled.", Toast.LENGTH_SHORT).show();
      }
    }  
  }

  
  /**
   * http://www.programcreek.com/java-api-examples/android.net.wifi.WifiManager 
   */
  @SimpleFunction(description = "Disable WiFi. You can hide the success message after setting the suppressSuccessMessage property to false.")
  public void Disable(){
    if (isRepl){
      Log.w(LOG_TAG, "You have to build the app to be able to use this method!");
      if (!suppressWarnings) {
        Toast.makeText(context, "You have to build the app to be able to use this method!", Toast.LENGTH_SHORT).show();
      }  
    } else {  
      wm.setWifiEnabled(false);
      if (!suppressSuccessMessage) {
        Toast.makeText(context,"WiFi disabled.",Toast.LENGTH_LONG).show();
      }
    }  
  }


  /*
   * http://stackoverflow.com/a/11075300
   */
  @SimpleFunction(description = "Get current WiFi SSID (Service Set Identifier).")
  public String SSID(){
    String ssid = "";
    ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    @SuppressWarnings("deprecation")
	NetworkInfo networkInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
    if (networkInfo.isConnected()) {
      final WifiInfo wi = wm.getConnectionInfo();
      if (wi != null && !TextUtils.isEmpty(wi.getSSID())) {
        ssid = wi.getSSID().replace("\"", ""); // replace double quotes in SSID
      }
    }
    
    if (!wm.isWifiEnabled()) {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't get current SSID.", Toast.LENGTH_SHORT).show();
      }  
    }
    
    Log.d(LOG_TAG, "SSID: " + ssid);
		return ssid;
  }


  /**
   * http://stackoverflow.com/a/20504821/1545993
   */
  @SimpleFunction(description = "Connect to a SSID (Service Set Identifier).")
  public void ConnectSSID(final String ssid, final String password){
    Log.d(LOG_TAG, "ConnectSSID");
    if (TextUtils.isEmpty(ssid)) {
      Log.d(LOG_TAG, "ssid is empty");
      if (!suppressWarnings) {
        Toast.makeText(context, "Can't connect to an empty SSID", Toast.LENGTH_SHORT).show();
      }  
    } else if (!wm.isWifiEnabled()) {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't connect to " + ssid, Toast.LENGTH_SHORT).show();
      }
    } else if (isRepl){
      if (!suppressWarnings) {
        Toast.makeText(context, "You have to build the app to be able to use this method!", Toast.LENGTH_SHORT).show();
      }  
    } else {
      AsynchUtil.runAsynchronously(new Runnable() {
        @Override
        public void run() {
        	AsyncConnectSSID(ssid, password);
        }
      });
    }
  }


  private void AsyncConnectSSID(String ssid, String password) {       
    Log.d(LOG_TAG, "AsyncConnectSSID");
  	WifiConfiguration wc = new WifiConfiguration();
    wc.SSID = String.format("\"%s\"", ssid);
    // 2018-04-11: connect without password
    if (password.isEmpty()) {
        // https://stackoverflow.com/a/37991589
      Log.v(LOG_TAG, "open network");
      wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
      wc.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
      wc.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
      wc.allowedAuthAlgorithms.clear();
      wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
      wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
    } else {
      Log.v(LOG_TAG, "WPA");
      // 2018-04-13
      wc.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
      wc.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
      wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
      wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
      wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
      wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
      wc.preSharedKey = String.format("\"%s\"", password);
    }
  
    
    // 2017-12-06
    //wm.disconnect(); //  in case you are already connected to another network
    // if you use boolean disableOthers true, then you don't have to disconnect or connect, it will do both for you
    //wm.enableNetwork(netId, true);
    //wm.reconnect();
    List<WifiConfiguration> list = wm.getConfiguredNetworks();
    boolean found = false;
    int netId = 0; // 2019-04-26
    for( WifiConfiguration i : list ) {
      if(i.SSID != null && i.SSID.equals("\"" + ssid + "\"")) {
        found = true;
        wm.disconnect();
        netId = i.networkId;
        Log.d(LOG_TAG, "ssid: " + ssid + " found in configured SSIDs, netId: " + netId);
        wm.enableNetwork(netId, true);
        wm.reconnect();               
        break;
      }           
    }
    if (!found) {
      //remember id
      netId = wm.addNetwork(wc);
      Log.d(LOG_TAG, "remember ssid: " + ssid + " in configured SSIDs, netId: " + netId);

      // 2017-12-14: if not found then connect the first time
      wm.disconnect(); //  in case you are already connected to another network
      wm.enableNetwork(netId, true);
      wm.reconnect();               
    }
    
    // 2019-04-26
    if (netId == -1) {
      activity.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            AfterWifiNegotiation(false);
          }
      });
    } else {
      final boolean successful = checkWifiNegotiation(netId);
      activity.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            AfterWifiNegotiation(successful);
          }
      });
    }
  }

  
  /**
   * http://stackoverflow.com/a/20772151/1545993
   * http://www.programcreek.com/java-api-examples/android.net.wifi.WifiManager example 6
   */
  @SimpleFunction(description = "Get a list of configured SSIDs (Service Set Identifiers). WiFi must be enabled for this.")
  public Object ConfiguredSSIDs(){
    Log.d(LOG_TAG, "ConfiguredSSIDs");
    try {
      List<WifiConfiguration> networks = wm.getConfiguredNetworks();
      List<String> ssids = new ArrayList<String>();
    
      if (networks != null){
        for (WifiConfiguration result : networks) {
          ssids.add(result.SSID.replace("\"", ""));  // replace double quotes in SSID
        }
      }
      return ssids;
    } catch (Exception e){
      if (!suppressWarnings) {
        Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();
      }  
      e.printStackTrace();
      Log.e(LOG_TAG, e.getMessage(), e);
      return null;
    }
  }

   
  
  /* ****************************************************************************************************************** */

  
  /*
   * Get MAC address 
   * min API Level 9 / Android 2.3
   * 
   * https://stackoverflow.com/a/10831640
   * https://stackoverflow.com/a/13007325
   * Note: wi.getMacAddress() does not work for Android M, see also http://stackoverflow.com/a/33165854
   */
  @SimpleFunction(description = "Get MAC address")
  public String MacAddress() {
    Log.d(LOG_TAG, "MacAddress");
    String macAddress = "02:00:00:00:00:00";
    WifiInfo wi = wm.getConnectionInfo();
    macAddress = wi.getMacAddress(); 

    if (macAddress.equals("02:00:00:00:00:00")) {
      Log.d(LOG_TAG, "MacAddress workaround");
      String interfaceName = "wlan0";
      try {
        List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
        for (NetworkInterface intf : interfaces) {
          if (interfaceName != null) {
            if (!intf.getName().equalsIgnoreCase(interfaceName)) continue;
          }
          byte[] mac = intf.getHardwareAddress();
          if (mac==null) return "02:00:00:00:00:00";
          StringBuilder buf = new StringBuilder();
          for (int idx=0; idx<mac.length; idx++)
            buf.append(String.format("%02X:", mac[idx]));       
          if (buf.length()>0) buf.deleteCharAt(buf.length()-1);
          return buf.toString();
        }
      } catch (Exception e) { 
        Log.e(LOG_TAG, e.getMessage(), e);
        if (!suppressWarnings){
          Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();  
        }
      }
      return "02:00:00:00:00:00";
    } else {
      return macAddress;  
    }  
  }

  

  
  /* ****************************************************************************************************************** */

  /* VERSION 3

  /* ****************************************************************************************************************** */

  /*
   * http://stackoverflow.com/a/6064347
   */
  @SimpleFunction(description = "Get current WiFi BSSID (the MAC address of the access point).")
  public String BSSID(){
    String bssid = "";
    ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    @SuppressWarnings("deprecation")
	NetworkInfo networkInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
    if (networkInfo.isConnected()) {
      final WifiInfo wi = wm.getConnectionInfo();
      if (wi != null && !TextUtils.isEmpty(wi.getBSSID())) {
        bssid = wi.getBSSID(); 
      }
    }
    
    if (!wm.isWifiEnabled()) {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't get current BSSID.", Toast.LENGTH_SHORT).show();
      }  
    }
    
    Log.d(LOG_TAG, "SSID: " + bssid);
    return bssid;
  }

  /* ****************************************************************************************************************** */

  /* VERSION 4

  /* ****************************************************************************************************************** */
 
  // https://stackoverflow.com/a/12029597
  @SimpleFunction(description = "Get signal strength (RSSI) in a range between 0 and 100.")
  public int SignalStrength(){
    Log.d(LOG_TAG, "SignalStrength");
    int MIN_RSSI        = -100;
    int MAX_RSSI        = -55;  
    int levels          = 101;
    WifiManager wifi    = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);            
    WifiInfo info       = wifi.getConnectionInfo(); 
    int rssi            = info.getRssi();

    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH){
      return WifiManager.calculateSignalLevel(info.getRssi(), levels);
    } else {             
      // this is the code since 4.0.1
      if (rssi <= MIN_RSSI) {
        return 0;
      } else if (rssi >= MAX_RSSI) {
        return levels - 1;
      } else {
        float inputRange = (MAX_RSSI - MIN_RSSI);
        float outputRange = (levels - 1);
        return (int)((float)(rssi - MIN_RSSI) * outputRange / inputRange);
      }
    }
  }
  
  
  @SimpleFunction(description = "Get current connection info.")
  public String ConnectionInfo(){
    Log.d(LOG_TAG, "ConnectionInfo");
    ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    @SuppressWarnings("deprecation")
	NetworkInfo networkInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

    if (!wm.isWifiEnabled()) {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't get current connection info.", Toast.LENGTH_SHORT).show();
      }  
    }
    
    String connectionInfo = "";
    if (networkInfo.isConnected()) {
      final WifiInfo wi = wm.getConnectionInfo();
      if (wi != null){
        connectionInfo = wi.toString();
      } 
    }  
    return connectionInfo;
  }

  
  @SimpleFunction(description = "Check, if 5 GHz Band is supported.")
  public boolean Is5GHzBandSupported(){
    Log.d(LOG_TAG, "Is5GHzBandSupported");

    if (!wm.isWifiEnabled()) {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't get check if 5 Ghz band is supported.", Toast.LENGTH_SHORT).show();
      }  
      return false;
    } else {
      return wm.is5GHzBandSupported();
    }
  }
  

  /* ****************************************************************************************************************** */

  /* VERSION 6

  /* ****************************************************************************************************************** */
  // https://stackoverflow.com/a/31308600
  @SimpleFunction(description = "Return the IP Address of access point.")
  public String AccessPointIP() {
    DhcpInfo dhcpInfo = wm.getDhcpInfo();
    byte[] ipAddress = convert2Bytes(dhcpInfo.serverAddress);
    try {
      String apIpAddr = InetAddress.getByAddress(ipAddress).getHostAddress();
      return apIpAddr;
    } catch (UnknownHostException e) {
      if (!suppressWarnings){
        Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show();  
      }
    }
    return "";
}

  
  private static byte[] convert2Bytes(int hostAddress) {
    byte[] addressBytes = { (byte)(0xff & hostAddress),
            (byte)(0xff & (hostAddress >> 8)),
            (byte)(0xff & (hostAddress >> 16)),
            (byte)(0xff & (hostAddress >> 24)) };
    return addressBytes;
  }

  /* ****************************************************************************************************************** */

  /* VERSION 8

  /* ****************************************************************************************************************** */
 
  // https://stackoverflow.com/a/38628311
  @SimpleFunction(description = "Disconnect.")
  public void Disconnect() {
    wm.disconnect();
  }

  /* ****************************************************************************************************************** */

  /* VERSION 9

  /* ****************************************************************************************************************** */

  @SimpleFunction(description = "Return a list of DNS servers (primary and secondary) of the current network.")
  public Object DnsServers() {
    Log.d(LOG_TAG, "DnsServers");

  	DnsServersDetector d = new DnsServersDetector();
  	String [] dnsArray = d.getServers();
    List<String> dnsList = new ArrayList<String>();
    for (String dns : dnsArray) {
      dnsList.add(dns.toString());
    }
    return dnsList;
  }


  
  /**
   * DNS servers detector
   *
   * IMPORTANT: don't cache the result.
   *
   * Or if you want to cache the result make sure you invalidate the cache
   * on any network change.
   *
   * It is always better to use a new instance of the detector when you need
   * current DNS servers otherwise you may get into troubles because of invalid/changed
   * DNS servers.
   *
   * This class combines various methods and solutions from:
   * Dnsjava http://www.xbill.org/dnsjava/
   * Minidns https://github.com/MiniDNS/minidns
   *
   * Unfortunately both libraries are not aware of Orero changes so new method was added to fix this.
   *
   * Created by Madalin Grigore-Enescu on 2/24/18.
   */
  
  //https://stackoverflow.com/a/48973823
  public class DnsServersDetector {

      private static final String TAG = "DnsServersDetector";

      /**
       * Holds some default DNS servers used in case all DNS servers detection methods fail.
       * Can be set to null if you want caller to fail in this situation.
       */
      private final String[] FACTORY_DNS_SERVERS = {
          "0.0.0.0",
          "0.0.0.0"
      };


      /**
       * Properties delimiter used in exec method of DNS servers detection
       */
      private static final String METHOD_EXEC_PROP_DELIM = "]: [";

			//private final String CONNECTIVITY_SERVICE = null;


      //region - public //////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Constructor
       */
      public DnsServersDetector() {
      }

      /**
       * Returns android DNS servers used for current connected network
       * @return Dns servers array
       */
      public String [] getServers() {

        // Will hold the consecutive result
        String[] result;

        // METHOD 1: old deprecated system properties
        result = getServersMethodSystemProperties();
        Log.d(LOG_TAG, "DNS, method1");
        if (result != null && result.length > 0) {
          Log.d(LOG_TAG, "result: " + result.toString());
          return result;
        }

        // METHOD 2 - use connectivity manager
        result = getServersMethodConnectivityManager();
        Log.d(LOG_TAG, "DNS, method2");
        if (result != null && result.length > 0) {
          Log.d(LOG_TAG, "result: " + result.toString());
          return result;
        }

        // LAST METHOD: detect android DNS servers by executing getprop string command in a separate process
        // This method fortunately works in Oreo too but many people may want to avoid exec
        // so it's used only as a failsafe scenario
        result = getServersMethodExec();
        Log.d(LOG_TAG, "DNS, method3");
        if (result != null && result.length > 0) {
          Log.d(LOG_TAG, "result: " + result.toString());
          return result;
        }

        // Fall back on factory DNS servers
        Log.d(LOG_TAG, "DNS, return default, " + FACTORY_DNS_SERVERS.toString());
        return FACTORY_DNS_SERVERS;
      }

      //endregion

      //region - private /////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Detect android DNS servers by using connectivity manager
       *
       * This method is working in android LOLLIPOP or later
       *
       * @return Dns servers array
       */
      private String [] getServersMethodConnectivityManager() {

          // This code only works on LOLLIPOP and higher
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

              try {

                  ArrayList<String> priorityServersArrayList  = new ArrayList<>();
                  ArrayList<String> serversArrayList          = new ArrayList<>();

                  ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);   
                  if (connectivityManager != null) {

                      // Iterate all networks
                      // Notice that android LOLLIPOP or higher allow iterating multiple connected networks of SAME type
                      for (Network network : connectivityManager.getAllNetworks()) {

                          NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
                          if (networkInfo.isConnected()) {

                              LinkProperties linkProperties    = connectivityManager.getLinkProperties(network);
                              List<InetAddress> dnsServersList = linkProperties.getDnsServers();

                              // Prioritize the DNS servers for link which have a default route
                              if (linkPropertiesHasDefaultRoute(linkProperties)) {

                                  for (InetAddress element: dnsServersList) {

                                      String dnsHost = element.getHostAddress();
                                      priorityServersArrayList.add(dnsHost);

                                  }

                              } else {

                                  for (InetAddress element: dnsServersList) {

                                      String dnsHost = element.getHostAddress();
                                      serversArrayList.add(dnsHost);

                                  }

                              }

                          }

                      }

                  }

                  // Append secondary arrays only if priority is empty
                  if (priorityServersArrayList.isEmpty()) {

                      priorityServersArrayList.addAll(serversArrayList);

                  }

                  // Stop here if we have at least one DNS server
                  if (priorityServersArrayList.size() > 0) {

                      return priorityServersArrayList.toArray(new String[0]);

                  }

              } catch (Exception ex) {

                  Log.d(TAG, "Exception detecting DNS servers using ConnectivityManager method", ex);

              }

          }

          // Failure
          return null;

      }

      /**
       * Detect android DNS servers by using old deprecated system properties
       *
       * This method is NOT working anymore in Android 8.0
       * Official Android documentation state this in the article Android 8.0 Behavior Changes.
       * The system properties net.dns1, net.dns2, net.dns3, and net.dns4 are no longer available,
       * a change that improves privacy on the platform.
       *
       * https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-pri
       * @return Dns servers array
       */
      private String [] getServersMethodSystemProperties() {


          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {

              // This originally looked for all lines containing .dns; but
              // http://code.google.com/p/android/issues/detail?id=2207#c73
              // indicates that net.dns* should always be the active nameservers, so
              // we use those.
              final String re1 = "^\\d+(\\.\\d+){3}$";
              final String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
              ArrayList<String> serversArrayList = new ArrayList<>();
              try {

                @SuppressWarnings("rawtypes")
				Class SystemProperties = Class.forName("android.os.SystemProperties");
                @SuppressWarnings("unchecked")
				Method method = SystemProperties.getMethod("get", new Class[]{String.class});
                  final String[] netdns = new String[]{"net.dns1", "net.dns2", "net.dns3", "net.dns4"};
                  for (int i = 0; i < netdns.length; i++) {

                      Object[] args = new Object[]{netdns[i]};
                      String v = (String) method.invoke(null, args);
                      if (v != null && (v.matches(re1) || v.matches(re2)) && !serversArrayList.contains(v)) {
                          serversArrayList.add(v);
                      }

                  }

                  // Stop here if we have at least one DNS server
                  if (serversArrayList.size() > 0) {

                      return serversArrayList.toArray(new String[0]);

                  }

              } catch (Exception ex) {

                  Log.d(TAG, "Exception detecting DNS servers using SystemProperties method", ex);

              }

          }

          // Failed
          return null;

      }

      /**
       * Detect android DNS servers by executing getprop string command in a separate process
       *
       * Notice there is an android bug when Runtime.exec() hangs without providing a Process object.
       * This problem is fixed in Jelly Bean (Android 4.1) but not in ICS (4.0.4) and probably it will never be fixed in ICS.
       * https://stackoverflow.com/questions/8688382/runtime-exec-bug-hangs-without-providing-a-process-object/11362081
       *
       * @return Dns servers array
       */
      private String [] getServersMethodExec() {

          // We are on the safe side and avoid any bug
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {

              try {

                  Process process = Runtime.getRuntime().exec("getprop");
                  InputStream inputStream = process.getInputStream();
                  LineNumberReader lineNumberReader = new LineNumberReader(new InputStreamReader(inputStream));
                  Set<String> serversSet = methodExecParseProps(lineNumberReader);
                  if (serversSet != null && serversSet.size() > 0) {

                      return serversSet.toArray(new String[0]);

                  }

              } catch (Exception ex) {

                  Log.d(TAG, "Exception in getServersMethodExec", ex);

              }

          }

          // Failed
          return null;

      }

      /**
       * Parse properties produced by executing getprop command
       * @param lineNumberReader
       * @return Set of parsed properties
       * @throws Exception
       */
      private Set<String> methodExecParseProps(BufferedReader lineNumberReader) throws Exception {

          String line;
          Set<String> serversSet = new HashSet<String>(10);

          while ((line = lineNumberReader.readLine()) != null) {
              int split = line.indexOf(METHOD_EXEC_PROP_DELIM);
              if (split == -1) {
                  continue;
              }
              String property = line.substring(1, split);

              int valueStart  = split + METHOD_EXEC_PROP_DELIM.length();
              int valueEnd    = line.length() - 1;
              if (valueEnd < valueStart) {

                  // This can happen if a newline sneaks in as the first character of the property value. For example
                  // "[propName]: [\n...]".
                  Log.d(TAG, "Malformed property detected: \"" + line + '"');
                  continue;

              }

              String value = line.substring(valueStart, valueEnd);

              if (value.isEmpty()) {

                  continue;

              }

              if (property.endsWith(".dns") || property.endsWith(".dns1") ||
                      property.endsWith(".dns2") || property.endsWith(".dns3") ||
                      property.endsWith(".dns4")) {

                  // normalize the address
                  InetAddress ip = InetAddress.getByName(value);
                  if (ip == null) continue;
                  value = ip.getHostAddress();

                  if (value == null) continue;
                  if (value.length() == 0) continue;

                  serversSet.add(value);

              }

          }

          return serversSet;

      }

      /**
       * Returns true if the specified link properties have any default route
       * @param linkProperties
       * @return true if the specified link properties have default route or false otherwise
       */
      @TargetApi(Build.VERSION_CODES.LOLLIPOP)
      private boolean linkPropertiesHasDefaultRoute(LinkProperties linkProperties) {

          for (RouteInfo route : linkProperties.getRoutes()) {
              if (route.isDefaultRoute()) {
                  return true;
              }
          }
          return false;

      }

      //endregion

  }
 
  
  /* ****************************************************************************************************************** */

  /* VERSION 11

  /* ****************************************************************************************************************** */

  
  /**
   * Get a list of available SSIDs
   * http://stackoverflow.com/a/11898482/1545993
   * http://stackoverflow.com/a/7527380/1545993
   */
  @SimpleFunction(description = "Get a list of available SSIDs (Service Set Identifiers). WiFi must be enabled for this.")
  public void AvailableSSIDs(){
    Log.d(LOG_TAG, "AvailableSSIDs called" );
    if (wm.isWifiEnabled()) {
      if (isRepl){
        Log.w(LOG_TAG, "You have to build the app to be able to use this method!");
        if (!suppressWarnings) {
          Toast.makeText(context, "You have to build the app to be able to use this method!", Toast.LENGTH_SHORT).show();
        }
      } else {
        wiFiReceiverScan = new WiFiReceiverScan();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        context.registerReceiver(wiFiReceiverScan, intentFilter);
        wm.startScan();
      }
    } else {
      if (!suppressWarnings) {
        Toast.makeText(context, "WiFi is disabled, can't get list of available SSIDs.", Toast.LENGTH_SHORT).show();
      }
      Log.d(LOG_TAG, "WiFi is disabled, exit." );
    }
  }

  
  /**
   * Event indicating that Available SSIDs have been scanned.
   */
  @SimpleEvent(description = "indicating that Available SSIDs (Service Set Identifiers) have been scanned.")
  public void GotAvailableSSIDs(Object availableSSIDs, String bestSSID, Object correspondingRSSIs, Object correspondingBSSIs) {
    Log.d(LOG_TAG, "GotAvailableSSIDs" );
    // invoke the application's "GotAvailableSSIDs" event handler.
    EventDispatcher.dispatchEvent(this, "GotAvailableSSIDs", availableSSIDs, bestSSID, correspondingRSSIs, correspondingBSSIs);
    unregisterReceiverScan();
  }


  private class WiFiReceiverScan extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
      Log.d(LOG_TAG, "onReceive, API version: " + Build.VERSION.SDK_INT);
      // probably check for M? http://stackoverflow.com/q/32151603/1545993
      List<ScanResult> scanresult = wm.getScanResults();
      List<String> ssids = new ArrayList<String>();
      List<String> rssis = new ArrayList<String>();
      List<String> bssis = new ArrayList<String>();
      
      ScanResult bestResult = null;
      String bestResultSSID = "";
      Log.d(LOG_TAG, "intent: " + intent + ", size: " + scanresult.size() + ", enabled: " + wm.isWifiEnabled() );
      
      for (ScanResult result : scanresult) {
        String ssid = result.SSID.replace("\"", "");  // replace double quotes in SSID 
        Log.d(LOG_TAG, ssid);
        ssids.add(ssid);  
        
        rssis.add(String.valueOf(result.level));  
        bssis.add(String.valueOf(result.BSSID));  
               
        // http://stackoverflow.com/a/11898482/1545993
        if(bestResult == null || WifiManager.compareSignalLevel(bestResult.level, result.level) < 0){
          bestResult = result;
          bestResultSSID = bestResult.SSID.replace("\"", "");
        }
      }
      GotAvailableSSIDs(ssids, bestResultSSID, rssis, bssis);
    }  
  }


  /**
   * Unregister
   */
  private void unregisterReceiverScan(){
    if (wiFiReceiverScan != null) {  // 2017-02-25
      Log.d(LOG_TAG, "unregisterReceiver");
      context.unregisterReceiver(wiFiReceiverScan);
      wiFiReceiverScan = null;       // 2017-05-10
    }
  }


  
  @Override
  public void onDestroy() {
    Log.d(LOG_TAG, "onDestroy");
    unregisterReceiverScan();
  }

  
  /* ****************************************************************************************************************** */

  /* VERSION 12

  /* ****************************************************************************************************************** */
  // https://stackoverflow.com/a/51872400
  private static boolean checkWifiNegotiation(int netId) {
    Log.d(LOG_TAG, "checkWifiNegotiation, netId: " + netId);
    boolean startedHandshake = false;
    boolean successful = false;

    for (int i = 0; i < 30; i++) {
      Log.d(LOG_TAG, "checkWifiNegotiation, i: " + i);
      try {
        Thread.sleep(300);
      } catch (InterruptedException e) {
        e.printStackTrace();
        Log.e(LOG_TAG, e.getMessage());
      }

      SupplicantState currentState = wm.getConnectionInfo().getSupplicantState();
      if (!startedHandshake && currentState.equals(SupplicantState.FOUR_WAY_HANDSHAKE)) {
        startedHandshake = true;
      } else if (startedHandshake) {
        if (currentState.equals(SupplicantState.DISCONNECTED)) {
          break;
        } else if (currentState.equals(SupplicantState.COMPLETED)) {
          successful = true;
          break;
        }
      }
      wm.enableNetwork(netId, true);
    }

	// no matter what happened above, if COMPLETED then we have the correct pw
	if (!successful && wm.getConnectionInfo().getSupplicantState().equals(SupplicantState.COMPLETED)) {
	  successful = true;
	}
    return successful;
  }

  /**
   * Indicates that the media has reached the end
   */
  @SimpleEvent(description = "Check if the negotiation with the WifiConfiguration was successful, Returns true or false.")
  public void AfterWifiNegotiation(boolean successful) {
    Log.d(LOG_TAG, "AfterWifiNegotiation" );
    EventDispatcher.dispatchEvent(this, "AfterWifiNegotiation", successful);
  }

  /* ****************************************************************************************************************** */

  /* VERSION 13

  /* ****************************************************************************************************************** */

  @SuppressWarnings("deprecation")
  /**
   * https://stackoverflow.com/a/16053255
   */
  @SimpleFunction(description = "Remove a SSID from the network list. "
  		+ "Note: starting from Android M, apps are not allowed to remove networks that they did not create.")
  public boolean RemoveSSID(final String ssid){
    Log.d(LOG_TAG, "RemoveSSID");
    boolean found = false;
    if (isRepl){
      if (!suppressWarnings) {
        Toast.makeText(context, "You have to build the app to be able to use this method!", Toast.LENGTH_SHORT).show();
      }  
    } else {
      List<WifiConfiguration> list = wm.getConfiguredNetworks();
      int netId = 0; // 
      for( WifiConfiguration i : list ) {
        if(i.SSID != null && i.SSID.equals("\"" + ssid + "\"")) {
          found = true;
          wm.disconnect();
          netId = i.networkId;
          Log.d(LOG_TAG, "ssid: " + ssid + " found in configured SSIDs, netId: " + netId);
          wm.removeNetwork(netId);
          wm.saveConfiguration();
          break;
        }  
      }           
    }
    return found;
  }  

}