diff options
Diffstat (limited to 'app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java')
-rw-r--r-- | app/src/fil/libre/repwifiapp/service/ConnectionManagementService.java | 720 |
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); + } + } + } + } + +} |