package org.fdroid.fdroid.net; import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.net.DhcpInfo; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; import org.apache.commons.net.util.SubnetUtils; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.localrepo.LocalRepoKeyStore; import org.fdroid.fdroid.localrepo.LocalRepoManager; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.security.cert.Certificate; import java.util.Enumeration; import java.util.Locale; /** * Handle state changes to the device's wifi, storing the required bits. * The {@link Intent} that starts it either has no extras included, * which is how it can be triggered by code, or it came in from the system * via {@link org.fdroid.fdroid.receiver.WifiStateChangeReceiver}, in * which case an instance of {@link NetworkInfo} is included. * * The work is done in a {@link Thread} so that new incoming {@code Intents} * are not blocked by processing. A new {@code Intent} immediately nullifies * the current state because it means that something about the wifi has * changed. Having the {@code Thread} also makes it easy to kill work * that is in progress. */ public class WifiStateChangeService extends IntentService { private static final String TAG = "WifiStateChangeService"; public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE"; private WifiManager wifiManager; private static WifiInfoThread wifiInfoThread; public WifiStateChangeService() { super("WifiStateChangeService"); } @Override protected void onHandleIntent(Intent intent) { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); if (intent == null) { Utils.debugLog(TAG, "received null Intent, ignoring"); return; } Utils.debugLog(TAG, "WiFi change service started, clearing info about wifi state until we have figured it out again."); NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); int wifiState = wifiManager.getWifiState(); if (ni == null || ni.isConnected()) { Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState)); if (wifiState == WifiManager.WIFI_STATE_ENABLED || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot || wifiState == WifiManager.WIFI_STATE_UNKNOWN) { // might be hotspot if (wifiInfoThread != null) { wifiInfoThread.interrupt(); } wifiInfoThread = new WifiInfoThread(); wifiInfoThread.start(); } } } public class WifiInfoThread extends Thread { private static final String TAG = "WifiInfoThread"; @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); try { FDroidApp.initWifiSettings(); Utils.debugLog(TAG, "Checking wifi state (in background thread)."); WifiInfo wifiInfo = null; int wifiState = wifiManager.getWifiState(); while (FDroidApp.ipAddressString == null) { if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver return; } if (wifiState == WifiManager.WIFI_STATE_ENABLED) { wifiInfo = wifiManager.getConnectionInfo(); FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress()); DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); if (dhcpInfo != null) { String netmask = formatIpAddress(dhcpInfo.netmask); if (!TextUtils.isEmpty(FDroidApp.ipAddressString) && netmask != null) { try { FDroidApp.subnetInfo = new SubnetUtils(FDroidApp.ipAddressString, netmask).getInfo(); } catch (IllegalArgumentException e) { // catch this mystery error: "java.lang.IllegalArgumentException: Could not parse [null/24]" e.printStackTrace(); } } } } else if (wifiState == WifiManager.WIFI_STATE_DISABLED || wifiState == WifiManager.WIFI_STATE_DISABLING) { // try once to see if its a hotspot setIpInfoFromNetworkInterface(); if (FDroidApp.ipAddressString == null) { return; } } else { // a hotspot can be active during WIFI_STATE_UNKNOWN setIpInfoFromNetworkInterface(); } if (FDroidApp.ipAddressString == null) { Thread.sleep(1000); Utils.debugLog(TAG, "waiting for an IP address..."); } } if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver return; } if (wifiInfo != null) { String ssid = wifiInfo.getSSID(); Utils.debugLog(TAG, "Have wifi info, connected to " + ssid); if (ssid != null) { FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1"); } String bssid = wifiInfo.getBSSID(); if (bssid != null) { FDroidApp.bssid = bssid; } } String scheme; if (Preferences.get().isLocalRepoHttpsEnabled()) { scheme = "https"; } else { scheme = "http"; } Repo repo = new Repo(); repo.name = Preferences.get().getLocalRepoName(); repo.address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo", scheme, FDroidApp.ipAddressString, FDroidApp.port); if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver return; } Context context = WifiStateChangeService.this.getApplicationContext(); LocalRepoManager lrm = LocalRepoManager.get(context); lrm.writeIndexPage(Utils.getSharingUri(FDroidApp.repo).toString()); if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver return; } // the fingerprint for the local repo's signing key LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context); Certificate localCert = localRepoKeyStore.getCertificate(); repo.fingerprint = Utils.calcFingerprint(localCert); FDroidApp.repo = repo; /* * Once the IP address is known we need to generate a self * signed certificate to use for HTTPS that has a CN field set * to the ipAddressString. This must be run in the background * because if this is the first time the singleton is run, it * can take a while to instantiate. */ if (Preferences.get().isLocalRepoHttpsEnabled()) { localRepoKeyStore.setupHTTPSCertificate(); } } catch (LocalRepoKeyStore.InitException e) { Log.e(TAG, "Unable to configure a fingerprint or HTTPS for the local repo", e); } catch (InterruptedException e) { Utils.debugLog(TAG, "interrupted"); return; } Intent intent = new Intent(BROADCAST); LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent); } } private void setIpInfoFromNetworkInterface() { try { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); if (networkInterfaces == null) { return; } while (networkInterfaces.hasMoreElements()) { NetworkInterface netIf = networkInterfaces.nextElement(); for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements();) { InetAddress inetAddress = inetAddresses.nextElement(); if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) { continue; } if (netIf.getDisplayName().contains("wlan0") || netIf.getDisplayName().contains("eth0") || netIf.getDisplayName().contains("ap0")) { FDroidApp.ipAddressString = inetAddress.getHostAddress(); for (InterfaceAddress address : netIf.getInterfaceAddresses()) { short networkPrefixLength = address.getNetworkPrefixLength(); if (networkPrefixLength > 32) { // something is giving a "/64" netmask, IPv6? // java.lang.IllegalArgumentException: Value [64] not in range [0,32] continue; } if (inetAddress.equals(address.getAddress()) && !TextUtils.isEmpty(FDroidApp.ipAddressString)) { String cidr = String.format(Locale.ENGLISH, "%s/%d", FDroidApp.ipAddressString, networkPrefixLength); FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo(); break; } } } } } } catch (SocketException e) { Log.e(TAG, "Could not get ip address", e); } } static String formatIpAddress(int ipAddress) { if (ipAddress == 0) { return null; } return String.format(Locale.ENGLISH, "%d.%d.%d.%d", ipAddress & 0xff, ipAddress >> 8 & 0xff, ipAddress >> 16 & 0xff, ipAddress >> 24 & 0xff); } private String printWifiState(int wifiState) { switch (wifiState) { case WifiManager.WIFI_STATE_DISABLED: return "WIFI_STATE_DISABLED"; case WifiManager.WIFI_STATE_DISABLING: return "WIFI_STATE_DISABLING"; case WifiManager.WIFI_STATE_ENABLING: return "WIFI_STATE_ENABLING"; case WifiManager.WIFI_STATE_ENABLED: return "WIFI_STATE_ENABLED"; case WifiManager.WIFI_STATE_UNKNOWN: return "WIFI_STATE_UNKNOWN"; } return null; } }