aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java')
-rw-r--r--app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java720
1 files changed, 720 insertions, 0 deletions
diff --git a/app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java b/app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java
new file mode 100644
index 0000000..2f70306
--- /dev/null
+++ b/app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java
@@ -0,0 +1,720 @@
+//
+// Copyright 2017, 2018 Filippo "Fil" Bergamo <fil.bergamo@riseup.net>
+//
+// This file is part of RepWifiApp.
+//
+// RepWifiApp is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// RepWifiApp is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with RepWifiApp. If not, see <http://www.gnu.org/licenses/>.
+//
+// ********************************************************************
+
+package fil.libre.repwifiapp.service;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import fil.libre.repwifiapp.Commons;
+import fil.libre.repwifiapp.Prefs;
+import fil.libre.repwifiapp.R;
+import fil.libre.repwifiapp.Utils;
+import fil.libre.repwifiapp.activities.MainActivity;
+import fil.libre.repwifiapp.fwproxies.LinkAddressProxy;
+import fil.libre.repwifiapp.fwproxies.LinkPropertiesProxy;
+import fil.libre.repwifiapp.fwproxies.NetworkCapabilitiesProxy;
+import fil.libre.repwifiapp.fwproxies.NetworkInfoProxy;
+import fil.libre.repwifiapp.fwproxies.RepWifiNetworkAgent;
+import fil.libre.repwifiapp.fwproxies.RouteInfoProxy;
+import fil.libre.repwifiapp.helpers.Logger;
+import fil.libre.repwifiapp.network.AccessPointInfo;
+import fil.libre.repwifiapp.network.ConnectionResult;
+import fil.libre.repwifiapp.network.ConnectionStatus;
+import fil.libre.repwifiapp.network.Engine6p0;
+import fil.libre.repwifiapp.network.IEngine;
+import fil.libre.repwifiapp.network.NetworkManager;
+import fil.libre.repwifiapp.network.WpaCli;
+import fil.libre.repwifiapp.network.WpaSupplicant;
+import fil.libre.repwifiapp.service.StatusManager.ConnectionStatusChangeListener;
+import org.apache.http.conn.util.InetAddressUtils;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+
+public class ConnectionManagementService extends Service implements ConnectionStatusChangeListener {
+
+ public static final String LOG_TAG_SERVICE = "RepWifiConnectionManagementService";
+
+ public static final String ACTION_DOMAIN = "fil.libre.repwifiapp.ConnectionService";
+ public static final String ACTION_CONNECT = ACTION_DOMAIN + ".ACTION_CONNECT";
+ public static final String ACTION_DISCONNECT = ACTION_DOMAIN + ".ACTION_DISCONNECT";
+ public static final String ACTION_VOID = ACTION_DOMAIN + ".ACTION_VOID";
+
+ public static final int MSG_BASE = 0;
+ public static final int CMD_START_CONNECT = MSG_BASE + 1;
+ public static final int CMD_ABORT_CONNECTION = MSG_BASE + 2;
+ public static final int CMD_DISCONNECT = MSG_BASE + 3;
+ public static final int CMD_GET_AVAILABLE_NETWORKS = MSG_BASE + 4;
+ public static final int CMD_START_MONITOR_CONNECTION_STATUS = MSG_BASE + 5;
+ public static final int CMD_STOP_MONITOR_CONNECTION_STATUS = MSG_BASE + 6;
+ public static final int CMD_STATUS_UPDATE = MSG_BASE + 7;
+ public static final int CMD_AUTOCONNECT = MSG_BASE + 8;
+ public static final int CMD_CLIENT_UNBINDING = MSG_BASE + 9;
+
+ /**
+ * TODO:
+ * Remove this command when a better model for the application's settings is
+ * implemented using @ContentProvider.
+ * For now, the UI will use this command to signal the service's process
+ * when a change is detected in the @SharedPreferences.
+ * A @ContentProvider should be used instead, to properly share the
+ * application's settings between the UI process and the background service.
+ * This command should be removed especially if the service is ever made
+ * "exported", i.e. available to other applications, in order to prevent
+ * external apps from tampering with the inner state of the service.
+ */
+ public static final int CMD_PREF_CHANGED = MSG_BASE + 10;
+
+ public static final int MSG_STATUS_CHANGE = MSG_BASE + 1001;
+ public static final int MSG_CONNECTION_RESULT = MSG_BASE + 1002;
+ public static final int MSG_AVAILABLE_NETWORKS = MSG_BASE + 1003;
+ public static final int MSG_AUTOCONNECT_REPORT = MSG_BASE + 1004;
+ public static final int MSG_DISCONNECT_REPORT = MSG_BASE + 1005;
+
+ /**
+ * This message is returned to a calling client, upon reception of a Command
+ * which the caller is not allowed to request, the original command id is
+ * reported in "arg1" field of this reply message.
+ */
+ public static final int MSG_PERMISSION_DENIED = MSG_BASE + 1403;
+
+ public static final int CHECK_STATUS_INTERVAL_SECS = 15;
+ public static final String PLACEHOLDER_CHECK_STATUS_INTERVAL = "[CHK_STS_INTERVAL]";
+
+ public static final String LOG_TAG_NETWORKAGENT = "RepWifiNetworkAgent";
+
+ private RepWifiNetworkAgent currentNetworkAgent = null;
+ private IEngine eng = null;
+ private StatusManager smonitor;
+ private ArrayList<Channel> statusWatchers = new ArrayList<Channel>();
+
+ private final Messenger messenger = new Messenger(new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+
+ Channel channel = new Channel(ConnectionManagementService.this, msg.replyTo);
+
+ switch (msg.what) {
+ case CMD_START_CONNECT:
+
+ AccessPointInfo info = channel.getAccessPointInfoPayload(msg);
+ if (info == null) {
+ Logger.logError("Received connect message without valid AccessPointInfo.");
+ } else {
+ connect(info, channel);
+ }
+ break;
+
+ case CMD_ABORT_CONNECTION:
+ abortConnection();
+ break;
+
+ case CMD_DISCONNECT:
+ disconnect(channel);
+ break;
+
+ case CMD_GET_AVAILABLE_NETWORKS:
+ getAvailableNetworks(channel);
+ break;
+
+ case CMD_START_MONITOR_CONNECTION_STATUS:
+ startMonitoringNetworkStatus(channel);
+ break;
+
+ case CMD_STOP_MONITOR_CONNECTION_STATUS:
+ stopMonitoringNetworkStatus(channel);
+ break;
+
+ case CMD_STATUS_UPDATE:
+ getStatus(channel);
+ break;
+
+ case CMD_CLIENT_UNBINDING:
+ onClientUnbinding(channel);
+ break;
+
+ case CMD_PREF_CHANGED:
+ String prefKey = channel.getStringPayload(msg, Channel.PAYLOAD_PREFKEY);
+ onPreferenceChanged(prefKey);
+ break;
+
+ default:
+ Logger.logError("Received message with unknown what: " + msg.what);
+ }
+ }
+
+ });
+
+ public ConnectionManagementService() {
+ }
+
+ @Override
+ public void onCreate() {
+
+ Commons.init(getApplicationContext());
+ initEngine();
+
+ Logger.APP_NAME = LOG_TAG_SERVICE;
+ Logger.setLogPriority(Prefs.getLogPriority(getApplicationContext()));
+
+ // Reset wpa_supplicant when the service is first run
+ // This is needed in case the internal WifiManager is controlling the
+ // current instance of wpa_supplicant.
+ Utils.killBackEnd(getApplicationContext(), true);
+
+ smonitor = new StatusManager(eng, this);
+
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if (intent == null) {
+ Logger.logDebug("[WRN]Service started with null intent!");
+ return START_STICKY;
+ }
+
+ String a = intent.getAction();
+
+ if (a == null) {
+ Logger.logDebug("[WRN] Service started with null action");
+ return START_STICKY;
+ }
+
+ Logger.logDebug("Service started with action: " + a);
+
+ if (a == ACTION_CONNECT) {
+ handleActionConnect(intent.getExtras());
+
+ } else if (a == ACTION_DISCONNECT) {
+ handleActionDisconnect();
+
+ } else if (a == ACTION_VOID) {
+ // Just start the service and wait for bindings
+ Logger.logDebug("Started with void action.");
+ } else {
+ Logger.logError("Unknown action " + a);
+ }
+
+ getStatus();
+ return START_STICKY;
+
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ smonitor.unsetListener();
+ }
+
+ private void handleActionConnect(Bundle xtras) {
+
+ if (xtras == null || xtras.containsKey(Channel.PAYLOAD_APINFO)) {
+ Logger.logError("Requested action connect without AccespointInfo extra!");
+ return;
+ }
+
+ try {
+ AccessPointInfo info = (AccessPointInfo) xtras.get(Channel.PAYLOAD_APINFO);
+
+ connect(info);
+
+ } catch (Exception e) {
+ Logger.logError("Exception while extracting AccessPointInfo object from start intent's extras.",
+ e);
+ }
+
+ }
+
+ private void handleActionDisconnect() {
+ disconnect();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return this.messenger.getBinder();
+ }
+
+ private ConnectionResult connect(AccessPointInfo info, Channel callback) {
+
+ initEngine();
+
+ int result = eng.connect(info);
+ ConnectionResult connectionResult = new ConnectionResult(result);
+ ConnectionStatus status = null;
+
+ if (result == ConnectionResult.CONN_OK) {
+
+ Logger.logDebug("Result code CONN_OK");
+
+ status = smonitor.getConnectionStatus();
+ connectionResult.setStatus(status);
+
+ if (info.needsPassword()) {
+
+ if (status != null) {
+ // update APinfo with the right BSSID
+ info.setBssid(status.BSSID);
+ }
+
+ // Save network
+ if (NetworkManager.save(info)) {
+ Logger.logDebug("Network saved: " + status.SSID);
+
+ } else {
+ Logger.logError("FAILED saving network: " + status.SSID);
+
+ }
+
+ }
+
+ }
+
+ reportConnectionResult(connectionResult, callback);
+ return connectionResult;
+ }
+
+ public ConnectionResult connect(AccessPointInfo info) {
+ return connect(info, null);
+ }
+
+ /**
+ * Attempts to connect to any nearby known network if found.
+ *
+ * @return Returns null if a network is found and connected.
+ * Returns an empty array if no network is found.
+ * Returns an array of AccessPointInfo if no nearby network is
+ * known.
+ */
+ public AccessPointInfo[] autoConnect() {
+ return autoConnect(null);
+ }
+
+ private AccessPointInfo[] autoConnect(Channel callback) {
+ try {
+
+ AccessPointInfo[] nets = eng.getAvailableNetworks();
+ if (nets == null || nets.length == 0) {
+ nets = new AccessPointInfo[] {};
+ }
+
+ for (AccessPointInfo i : nets) {
+
+ if (NetworkManager.isKnown(i)) {
+ connect(i, null);
+ nets = null;
+ }
+
+ }
+
+ // if no network is known, return available networks:
+ if (callback != null) {
+ reportAutoconnectResult(nets, callback);
+ }
+ return nets;
+
+ } catch (Exception e) {
+ Logger.logError("Error while autoconnecting", e);
+ return null;
+ }
+ }
+
+ private ConnectionStatus getStatus(Channel callback) {
+
+ ConnectionStatus status = smonitor.getConnectionStatus();
+
+ if (callback != null) {
+ callback.sendMsg(MSG_STATUS_CHANGE, status, Channel.PAYLOAD_CONNSTATUS);
+ }
+ return status;
+
+ }
+
+ public ConnectionStatus getStatus() {
+ return getStatus(null);
+ }
+
+ public void abortConnection() {
+ initEngine();
+ eng.abortConnection();
+ }
+
+ public boolean disconnect() {
+ return disconnect(null);
+ }
+
+ private boolean disconnect(Channel callback) {
+
+ initEngine();
+ boolean res = eng.disconnect();
+ ConnectionStatus status = getStatus();
+
+ if (callback != null) {
+ callback.sendMsg(MSG_DISCONNECT_REPORT, status, Channel.PAYLOAD_CONNSTATUS);
+ }
+
+ return res;
+ }
+
+ private AccessPointInfo[] getAvailableNetworks(Channel callback) {
+ initEngine();
+ AccessPointInfo[] nets = eng.getAvailableNetworks();
+ if (callback != null) {
+ callback.sendMsg(MSG_AVAILABLE_NETWORKS, nets, Channel.PAYLOAD_APINFO);
+ }
+ return nets;
+ }
+
+ public AccessPointInfo[] getAvailableNetworks() {
+ return getAvailableNetworks(null);
+ }
+
+ private void initEngine() {
+ if (eng == null) {
+ eng = new Engine6p0();
+ }
+ }
+
+ private void reportConnectionResult(ConnectionResult result, Channel callback) {
+ callback.sendMsg(MSG_CONNECTION_RESULT, result, Channel.PAYLOAD_CONNRES);
+ }
+
+ private void reportAutoconnectResult(AccessPointInfo[] infos, Channel callback) {
+ callback.sendMsg(MSG_AUTOCONNECT_REPORT, infos, Channel.PAYLOAD_APINFO);
+ }
+
+ private static final int NOTIFICATION_ID = 1;
+
+ @Override
+ public void onConnectionStatusChange(ConnectionStatus status) {
+
+ Logger.logDebug("Received connection status changed");
+ notifyWifiState(status);
+ updateNotification(status);
+ reportNetworkStatus(status);
+
+ }
+
+ private void onClientUnbinding(Channel client) {
+
+ if (client == null) {
+ return;
+ }
+
+ Logger.logDebug("Processing client unbinding.. ");
+ stopMonitoringNetworkStatus(client);
+
+ }
+
+ private boolean notifyWifiState(ConnectionStatus status) {
+
+ try {
+
+ if (status == null) {
+ Logger.logDebug("Received null ConnectionStatus; using dummy status disconnected.");
+ status = ConnectionStatus.getDummyDisconnected();
+ }
+
+ Logger.logDebug("Notifying wifi state with status object: " + status.toString());
+
+ NetworkInfoProxy ni = NetworkInfoProxy.getForWifi();
+ NetworkCapabilitiesProxy nc = new NetworkCapabilitiesProxy();
+ LinkPropertiesProxy lp = new LinkPropertiesProxy();
+ lp.setInterfaceName(WpaSupplicant.INTERFACE_NAME);
+
+ if (status.isConnected()) {
+
+ ni.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null, null);
+ ni.setIsAvailable(true);
+
+ if (!lp.addLinkAddress(new LinkAddressProxy(status.getInetAddress(), status
+ .getSubnetMaskInt()))) {
+ Logger.logError("Failed to add LinkAddress to LinkProperties.");
+ return false;
+ }
+
+ if (!lp.addRoute(new RouteInfoProxy(status.getGatewayInetAddress(),
+ WpaSupplicant.INTERFACE_NAME))) {
+ Logger.logError("Failed to add route to linkProperties");
+ return false;
+ }
+
+ InetAddress[] dnss = getUseableDnss(status.gateway);
+
+ if (dnss == null || dnss.length == 0) {
+ Logger.logError("Received null or empty dns array");
+ return false;
+ }
+
+ for (InetAddress d : dnss) {
+ if (d != null && !lp.addDnsServer(d)) {
+ Logger.logError("Failed to add dns to LinkProperties.");
+ return false;
+ }
+ }
+
+ nc.addCapability(NetworkCapabilitiesProxy.NET_CAPABILITY_NOT_METERED);
+ nc.addCapability(NetworkCapabilitiesProxy.NET_CAPABILITY_INTERNET);
+
+ if (!agentIsAvailable()) {
+ Logger.logDebug("Willing to communicate netwtork connection, but no NetworkAgent available. Creating new NetworkAgent..");
+ createNetworkAgent(ni, lp, nc, 100);
+ }
+
+ } else if (agentIsAvailable()) {
+ ni.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, null, null);
+ ni.setIsAvailable(true);
+
+ } else {
+ // status is "disconnected" and we have no active communication
+ // channel with the ConnectivityService.
+ // no need to establish a new channel just to communicate
+ // disconnection:
+ // ConnectivityService should know we're disconnected already.
+ return true;
+ }
+
+ Logger.logDebug("About to call NetworkAgent.sendNetworkIngfo() connected="
+ + status.isConnected());
+
+ currentNetworkAgent.sendNetworkInfo(ni.getNetworkInfo());
+ Logger.logDebug("Called NetworkAgent.sendNetworkIngfo()..");
+
+ return true;
+
+ } catch (Exception e) {
+ Logger.logError("FAIL registerNetworkAgent", e);
+ return false;
+ }
+
+ }
+
+ private boolean agentIsAvailable() {
+ return (currentNetworkAgent != null && currentNetworkAgent.isChannellConnected());
+ }
+
+ private int createNetworkAgent(NetworkInfoProxy ni, LinkPropertiesProxy lp,
+ NetworkCapabilitiesProxy nc, int score) {
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ Logger.logDebug("About to create new RepWifiNetworkAgent...");
+ try {
+
+ currentNetworkAgent = new RepWifiNetworkAgent(Looper.myLooper(),
+ getApplicationContext().getApplicationContext(), LOG_TAG_NETWORKAGENT,
+ ni.getNetworkInfo(), nc, lp, score);
+ Logger.logDebug("Created RepWifiNetworkAgent, netId: " + currentNetworkAgent.netId);
+ return currentNetworkAgent.netId;
+
+ } catch (Exception e) {
+ Logger.logError("Exception while creating RepWifiNetworkAgent", e);
+ return -1;
+ }
+
+ }
+
+ private String[] getConfiguredDnss() {
+
+ // no more default DNS, it's a stupid thing to do.
+ // instead, default to empty dns (using gateway as dns)
+ // it should be up to the user to chose their own dns explicitly.
+ String dns1 = Prefs.getString(getApplicationContext(),Prefs.PREF_DNS_1, "");
+ String dns2 = Prefs.getString(getApplicationContext(), Prefs.PREF_DNS_2, "");
+
+ if (dns1 == null || dns1.isEmpty()) {
+ return null;
+ }
+
+ return new String[] { dns1, dns2 };
+
+ }
+
+ private InetAddress[] getUseableDnss(String gateway) {
+
+ String[] dnss = getConfiguredDnss();
+
+ if (dnss == null || dnss.length == 0) {
+ // the DNS setting has been left blank
+ // try to use the gateway as dns
+
+ if (gateway == null || gateway.length() == 0) {
+ // no possible DNS.
+ return null;
+ }
+
+ dnss = new String[] { gateway, null };
+
+ }
+
+ InetAddress d1 = null;
+ InetAddress d2 = null;
+
+ if (InetAddressUtils.isIPv4Address(dnss[0])) {
+ try {
+
+ d1 = InetAddress.getByName(dnss[0]);
+
+ if (dnss[1] != null && InetAddressUtils.isIPv4Address(dnss[1])) {
+
+ d2 = InetAddress.getByName(dnss[1]);
+
+ }
+
+ } catch (UnknownHostException e) {
+ Logger.logError("Exception while parsing dns address!", e);
+ return null;
+ }
+
+ return new InetAddress[] { d1, d2 };
+
+ } else {
+ Logger.logError("Wrong dns1 format!");
+ return null;
+ }
+
+ }
+
+ private void updateNotification(ConnectionStatus status) {
+
+ if (status == null) {
+ status = WpaCli.getConnectionStatus();
+ }
+
+ Notification.Builder builder = new Notification.Builder(getApplicationContext());
+
+ Intent intent = new Intent(getApplicationContext(), MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent,
+ 0);
+ builder.setContentIntent(pendingIntent);
+
+ int iconId = R.drawable.ic_stat_discon;
+ String msg = "RepWifi";
+ if (status != null) {
+ if (status.isConnected()) {
+ iconId = R.drawable.ic_stat_repwifi;
+ msg += " - " + status.SSID;
+ } else {
+ msg += " - " + status.wpaStatus;
+ }
+
+ }
+
+ builder.setSmallIcon(iconId);
+
+ builder.setContentTitle(msg);
+ builder.setContentText(getString(R.string.msg_touch_open));
+
+ Notification n = builder.build();
+ n.flags |= Notification.FLAG_NO_CLEAR;
+
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_ID, n);
+
+ }
+
+ private void onPreferenceChanged(String prefName) {
+
+ if (prefName == null) {
+ Logger.logError("Received preference changed event, but prefName is null!");
+ return;
+ }
+
+ if (prefName.equals(Prefs.PREF_MONITOR_NET_STATE)) {
+ setMonitorNetworkStatus(Prefs.isNetworkStateMonitoringEnabled(getApplicationContext()));
+
+ } else if (prefName.equals(Prefs.PREF_LOG_LEVEL)) {
+ Logger.setLogPriority(Prefs.getLogPriority(getApplicationContext()));
+
+ }
+
+ }
+
+ private boolean monitoringExplicitlyEnabled = false;
+
+ private void setMonitorNetworkStatus(boolean enabled) {
+
+ monitoringExplicitlyEnabled = enabled;
+ if (enabled) {
+ startMonitoringNetworkStatus(null);
+
+ } else {
+ stopMonitoringNetworkStatus(null);
+
+ }
+ }
+
+ private void startMonitoringNetworkStatus(Channel watcher) {
+
+ synchronized (statusWatchers) {
+ if (watcher != null && !statusWatchers.contains(watcher)) {
+ Logger.logDebug("Added watcher for network status: " + watcher.toString());
+ statusWatchers.add(watcher);
+ }
+ }
+
+ smonitor.startPolling(CHECK_STATUS_INTERVAL_SECS * 1000);
+
+ }
+
+ private void stopMonitoringNetworkStatus(Channel watcher) {
+
+ synchronized (statusWatchers) {
+
+ if (watcher != null && statusWatchers.remove(watcher)) {
+ Logger.logDebug("Removed watcher for network status: " + watcher.toString());
+ }
+
+ if (statusWatchers.isEmpty() && !monitoringExplicitlyEnabled) {
+ if (smonitor != null) {
+ smonitor.stopPolling();
+ }
+ }
+ }
+
+ }
+
+ private void reportNetworkStatus(ConnectionStatus status) {
+
+ synchronized (statusWatchers) {
+
+ for (Channel m : statusWatchers) {
+ if (!m.sendMsg(MSG_STATUS_CHANGE, status, Channel.PAYLOAD_CONNSTATUS)) {
+ // remove recipient from watchers as it's not able to
+ // receive messages anymore
+ statusWatchers.remove(m);
+ }
+ }
+ }
+ }
+
+}