PageRenderTime 26ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/app/src/full/java/org/fdroid/fdroid/net/WifiStateChangeService.java

https://gitlab.com/pserwylo/fdroidclient
Java | 320 lines | 249 code | 27 blank | 44 comment | 74 complexity | afcbe2c194a3061a1a77e477946df1c0 MD5 | raw file
Possible License(s): GPL-3.0
  1. package org.fdroid.fdroid.net;
  2. import android.app.IntentService;
  3. import android.content.ComponentName;
  4. import android.content.Context;
  5. import android.content.Intent;
  6. import android.net.DhcpInfo;
  7. import android.net.NetworkInfo;
  8. import android.net.wifi.WifiInfo;
  9. import android.net.wifi.WifiManager;
  10. import android.os.Build;
  11. import android.support.annotation.Nullable;
  12. import android.support.v4.content.LocalBroadcastManager;
  13. import android.text.TextUtils;
  14. import android.util.Log;
  15. import org.apache.commons.net.util.SubnetUtils;
  16. import org.fdroid.fdroid.BuildConfig;
  17. import org.fdroid.fdroid.FDroidApp;
  18. import org.fdroid.fdroid.Preferences;
  19. import org.fdroid.fdroid.UpdateService;
  20. import org.fdroid.fdroid.Utils;
  21. import org.fdroid.fdroid.data.Repo;
  22. import org.fdroid.fdroid.localrepo.LocalRepoKeyStore;
  23. import org.fdroid.fdroid.localrepo.LocalRepoManager;
  24. import java.net.Inet6Address;
  25. import java.net.InetAddress;
  26. import java.net.InterfaceAddress;
  27. import java.net.NetworkInterface;
  28. import java.net.SocketException;
  29. import java.security.cert.Certificate;
  30. import java.util.Enumeration;
  31. import java.util.Locale;
  32. /**
  33. * Handle state changes to the device's wifi, storing the required bits.
  34. * The {@link Intent} that starts it either has no extras included,
  35. * which is how it can be triggered by code, or it came in from the system
  36. * via {@link org.fdroid.fdroid.receiver.WifiStateChangeReceiver}, in
  37. * which case an instance of {@link NetworkInfo} is included.
  38. * <p>
  39. * The work is done in a {@link Thread} so that new incoming {@code Intents}
  40. * are not blocked by processing. A new {@code Intent} immediately nullifies
  41. * the current state because it means that something about the wifi has
  42. * changed. Having the {@code Thread} also makes it easy to kill work
  43. * that is in progress.
  44. * <p>
  45. * This also schedules an update to encourage updates happening on
  46. * unmetered networks like typical WiFi rather than networks that can
  47. * cost money or have caps. The logic for checking the state of the
  48. * internet connection is in {@link org.fdroid.fdroid.UpdateService#onHandleWork(Intent)}
  49. * <p>
  50. * Some devices send multiple copies of given events, like a Moto G often
  51. * sends three {@code CONNECTED} events. So they have to be debounced to
  52. * keep the {@link #BROADCAST} useful.
  53. */
  54. @SuppressWarnings("LineLength")
  55. public class WifiStateChangeService extends IntentService {
  56. private static final String TAG = "WifiStateChangeService";
  57. public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
  58. private WifiManager wifiManager;
  59. private static WifiInfoThread wifiInfoThread;
  60. private static int previousWifiState = Integer.MIN_VALUE;
  61. public WifiStateChangeService() {
  62. super("WifiStateChangeService");
  63. }
  64. public static void start(Context context, @Nullable Intent intent) {
  65. if (intent == null) {
  66. intent = new Intent(WifiManager.NETWORK_STATE_CHANGED_ACTION);
  67. }
  68. intent.setComponent(new ComponentName(context, WifiStateChangeService.class));
  69. context.startService(intent);
  70. }
  71. @Override
  72. protected void onHandleIntent(Intent intent) {
  73. android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
  74. if (intent == null) {
  75. Utils.debugLog(TAG, "received null Intent, ignoring");
  76. return;
  77. }
  78. Utils.debugLog(TAG, "WiFi change service started.");
  79. NetworkInfo ni = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
  80. wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
  81. int wifiState = wifiManager.getWifiState();
  82. if (ni == null || ni.isConnected()) {
  83. Utils.debugLog(TAG, "ni == " + ni + " wifiState == " + printWifiState(wifiState));
  84. if (previousWifiState != wifiState &&
  85. (wifiState == WifiManager.WIFI_STATE_ENABLED
  86. || wifiState == WifiManager.WIFI_STATE_DISABLING // might be switching to hotspot
  87. || wifiState == WifiManager.WIFI_STATE_DISABLED // might be hotspot
  88. || wifiState == WifiManager.WIFI_STATE_UNKNOWN)) { // might be hotspot
  89. if (wifiInfoThread != null) {
  90. wifiInfoThread.interrupt();
  91. }
  92. wifiInfoThread = new WifiInfoThread();
  93. wifiInfoThread.start();
  94. }
  95. if (Build.VERSION.SDK_INT < 21 && wifiState == WifiManager.WIFI_STATE_ENABLED) {
  96. UpdateService.scheduleIfStillOnWifi(this);
  97. }
  98. }
  99. }
  100. public class WifiInfoThread extends Thread {
  101. private static final String TAG = "WifiInfoThread";
  102. @Override
  103. public void run() {
  104. android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST);
  105. try {
  106. FDroidApp.initWifiSettings();
  107. Utils.debugLog(TAG, "Checking wifi state (in background thread).");
  108. WifiInfo wifiInfo = null;
  109. int wifiState = wifiManager.getWifiState();
  110. int retryCount = 0;
  111. while (FDroidApp.ipAddressString == null) {
  112. if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver
  113. return;
  114. }
  115. if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
  116. wifiInfo = wifiManager.getConnectionInfo();
  117. FDroidApp.ipAddressString = formatIpAddress(wifiInfo.getIpAddress());
  118. DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
  119. if (dhcpInfo != null) {
  120. String netmask = formatIpAddress(dhcpInfo.netmask);
  121. if (!TextUtils.isEmpty(FDroidApp.ipAddressString) && netmask != null) {
  122. try {
  123. FDroidApp.subnetInfo = new SubnetUtils(FDroidApp.ipAddressString, netmask).getInfo();
  124. } catch (IllegalArgumentException e) {
  125. // catch mystery: "java.lang.IllegalArgumentException: Could not parse [null/24]"
  126. e.printStackTrace();
  127. }
  128. }
  129. }
  130. if (FDroidApp.ipAddressString == null
  131. || FDroidApp.subnetInfo == FDroidApp.UNSET_SUBNET_INFO) {
  132. setIpInfoFromNetworkInterface();
  133. }
  134. } else if (wifiState == WifiManager.WIFI_STATE_DISABLED
  135. || wifiState == WifiManager.WIFI_STATE_DISABLING) {
  136. // try once to see if its a hotspot
  137. setIpInfoFromNetworkInterface();
  138. if (FDroidApp.ipAddressString == null) {
  139. return;
  140. }
  141. } else { // a hotspot can be active during WIFI_STATE_UNKNOWN
  142. setIpInfoFromNetworkInterface();
  143. }
  144. if (retryCount > 120) {
  145. return;
  146. }
  147. retryCount++;
  148. if (FDroidApp.ipAddressString == null) {
  149. Thread.sleep(1000);
  150. Utils.debugLog(TAG, "waiting for an IP address...");
  151. }
  152. }
  153. if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver
  154. return;
  155. }
  156. if (wifiInfo != null) {
  157. String ssid = wifiInfo.getSSID();
  158. Utils.debugLog(TAG, "Have wifi info, connected to " + ssid);
  159. if (ssid != null) {
  160. FDroidApp.ssid = ssid.replaceAll("^\"(.*)\"$", "$1");
  161. }
  162. String bssid = wifiInfo.getBSSID();
  163. if (bssid != null) {
  164. FDroidApp.bssid = bssid;
  165. }
  166. }
  167. String scheme;
  168. if (Preferences.get().isLocalRepoHttpsEnabled()) {
  169. scheme = "https";
  170. } else {
  171. scheme = "http";
  172. }
  173. Repo repo = new Repo();
  174. repo.name = Preferences.get().getLocalRepoName();
  175. repo.address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo",
  176. scheme, FDroidApp.ipAddressString, FDroidApp.port);
  177. if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver
  178. return;
  179. }
  180. Context context = WifiStateChangeService.this.getApplicationContext();
  181. LocalRepoManager lrm = LocalRepoManager.get(context);
  182. lrm.writeIndexPage(Utils.getSharingUri(FDroidApp.repo).toString());
  183. if (isInterrupted()) { // can be canceled by a change via WifiStateChangeReceiver
  184. return;
  185. }
  186. // the fingerprint for the local repo's signing key
  187. LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
  188. Certificate localCert = localRepoKeyStore.getCertificate();
  189. repo.fingerprint = Utils.calcFingerprint(localCert);
  190. FDroidApp.repo = repo;
  191. /*
  192. * Once the IP address is known we need to generate a self
  193. * signed certificate to use for HTTPS that has a CN field set
  194. * to the ipAddressString. This must be run in the background
  195. * because if this is the first time the singleton is run, it
  196. * can take a while to instantiate.
  197. */
  198. if (Preferences.get().isLocalRepoHttpsEnabled()) {
  199. localRepoKeyStore.setupHTTPSCertificate();
  200. }
  201. } catch (LocalRepoKeyStore.InitException e) {
  202. Log.e(TAG, "Unable to configure a fingerprint or HTTPS for the local repo", e);
  203. } catch (InterruptedException e) {
  204. Utils.debugLog(TAG, "interrupted");
  205. return;
  206. }
  207. Intent intent = new Intent(BROADCAST);
  208. LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
  209. }
  210. }
  211. /**
  212. * Search for known Wi-Fi, Hotspot, and local network interfaces and get
  213. * the IP Address info from it. This is necessary because network
  214. * interfaces in Hotspot/AP mode do not show up in the regular
  215. * {@link WifiManager} queries, and also on
  216. * {@link android.os.Build.VERSION_CODES#LOLLIPOP Android 5.0} and newer,
  217. * {@link WifiManager#getDhcpInfo()} returns an invalid netmask.
  218. *
  219. * @see <a href="https://issuetracker.google.com/issues/37015180">netmask of WifiManager.getDhcpInfo() is always zero on Android 5.0</a>
  220. */
  221. private void setIpInfoFromNetworkInterface() {
  222. try {
  223. Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
  224. if (networkInterfaces == null) {
  225. return;
  226. }
  227. while (networkInterfaces.hasMoreElements()) {
  228. NetworkInterface netIf = networkInterfaces.nextElement();
  229. for (Enumeration<InetAddress> inetAddresses = netIf.getInetAddresses(); inetAddresses.hasMoreElements();) {
  230. InetAddress inetAddress = inetAddresses.nextElement();
  231. if (inetAddress.isLoopbackAddress() || inetAddress instanceof Inet6Address) {
  232. continue;
  233. }
  234. if (netIf.getDisplayName().contains("wlan0")
  235. || netIf.getDisplayName().contains("eth0")
  236. || netIf.getDisplayName().contains("ap0")) {
  237. FDroidApp.ipAddressString = inetAddress.getHostAddress();
  238. for (InterfaceAddress address : netIf.getInterfaceAddresses()) {
  239. short networkPrefixLength = address.getNetworkPrefixLength();
  240. if (networkPrefixLength > 32) {
  241. // something is giving a "/64" netmask, IPv6?
  242. // java.lang.IllegalArgumentException: Value [64] not in range [0,32]
  243. continue;
  244. }
  245. try {
  246. String cidr = String.format(Locale.ENGLISH, "%s/%d",
  247. FDroidApp.ipAddressString, networkPrefixLength);
  248. FDroidApp.subnetInfo = new SubnetUtils(cidr).getInfo();
  249. break;
  250. } catch (IllegalArgumentException e) {
  251. if (BuildConfig.DEBUG) {
  252. e.printStackTrace();
  253. } else {
  254. Log.i(TAG, e.getLocalizedMessage());
  255. }
  256. }
  257. }
  258. }
  259. }
  260. }
  261. } catch (SocketException e) {
  262. Log.e(TAG, "Could not get ip address", e);
  263. }
  264. }
  265. static String formatIpAddress(int ipAddress) {
  266. if (ipAddress == 0) {
  267. return null;
  268. }
  269. return String.format(Locale.ENGLISH, "%d.%d.%d.%d",
  270. ipAddress & 0xff,
  271. ipAddress >> 8 & 0xff,
  272. ipAddress >> 16 & 0xff,
  273. ipAddress >> 24 & 0xff);
  274. }
  275. private String printWifiState(int wifiState) {
  276. switch (wifiState) {
  277. case WifiManager.WIFI_STATE_DISABLED:
  278. return "WIFI_STATE_DISABLED";
  279. case WifiManager.WIFI_STATE_DISABLING:
  280. return "WIFI_STATE_DISABLING";
  281. case WifiManager.WIFI_STATE_ENABLING:
  282. return "WIFI_STATE_ENABLING";
  283. case WifiManager.WIFI_STATE_ENABLED:
  284. return "WIFI_STATE_ENABLED";
  285. case WifiManager.WIFI_STATE_UNKNOWN:
  286. return "WIFI_STATE_UNKNOWN";
  287. case Integer.MIN_VALUE:
  288. return "previous value unset";
  289. default:
  290. return "~not mapped~";
  291. }
  292. }
  293. }