package de.fun2code.android.piratebox; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.paw.server.PawServer; import android.app.Notification; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.text.format.Formatter; import android.util.Log; import android.widget.Toast; import de.fun2code.android.pawserver.PawServerService; import de.fun2code.android.pawserver.listener.ServiceListener; import de.fun2code.android.pawserver.service.ServiceCompat; import de.fun2code.android.piratebox.util.NetworkUtil; import de.fun2code.android.piratebox.util.NetworkUtil.IpTablesAction; import de.fun2code.android.piratebox.util.NetworkUtil.WrapResult; import de.fun2code.android.piratebox.util.ServerConfigUtil; import de.fun2code.android.piratebox.util.ShellUtil; import de.fun2code.android.piratebox.widget.PirateBoxWidget; /** * The PirateBox serice class which handles the access point, network configuration * and startup up the web server. * * @author joschi * */ public class PirateBoxService extends PawServerService implements ServiceListener { private WifiConfiguration orgApConfig; private boolean orgWifiState; //private boolean orgMobileDataState; // Not needed private NetworkUtil netUtil; private ShellUtil shellUtil; private PirateBoxService service; private SharedPreferences preferences; private boolean emulateDroopy = true; private boolean externalServer = false; private int EXTERNAL_SERVER_NOTIFICATION_ID = PirateBoxService.class.toString().hashCode(); public static boolean externalServerRunning = false; public static boolean autoApStartup = true; public static boolean configureAp = true; private static List<StateChangedListener> listeners = new ArrayList<StateChangedListener>(); private static boolean apRunning, networkRunning, startingUp; /** * Broadcast receiver which receives access point state change notifications */ private final BroadcastReceiver apReceiver = new BroadcastReceiver() { static final int WIFI_AP_STATE_DISABLING = 10; static final int WIFI_AP_STATE_DISABLED = 11; static final int WIFI_AP_STATE_ENABLED = 13; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // If access point state changed if (action.equals("android.net.wifi.WIFI_AP_STATE_CHANGED")) { int state = intent.getIntExtra("wifi_state", WifiManager.WIFI_STATE_UNKNOWN); switch(state) { // If access point was enabled case WIFI_AP_STATE_ENABLED: case WifiManager.WIFI_STATE_ENABLED: if(!apRunning) { apRunning = true; int pid = shellUtil.waitForProcess(NetworkUtil.DNSMASQ_BIN_BACKUP, 4000); Log.i(TAG, "Process ID of " + NetworkUtil.DNSMASQ_BIN_BACKUP + ": " + pid); // Restore dnsmasq netUtil.unwrapDnsmasq(); // Inform about unwrap result for(StateChangedListener listener : listeners) { listener.dnsMasqUnWrapped(); } /* * Check if dnsmasq is running ok. * This might not be the case on Android 5 and up. * In such a case, try to restart dnsmasq manually */ if(!netUtil.isDnsMasqRunning()) { netUtil.restartDnsMasq(NetworkUtil.getApIp(service)); } /* * Restore AP state * Only restore if configuration was changed. */ if(configureAp) { netUtil.setWifiApConfiguration(orgApConfig); } for(StateChangedListener listener : listeners) { listener.apEnabled(autoApStartup); } Intent apUpIntent = new Intent(Constants.BROADCAST_INTENT_AP); apUpIntent.putExtra(Constants.INTENT_AP_EXTRA_STATE, true); sendBroadcast(apUpIntent); Intent serviceIntent = new Intent(service, PirateBoxService.class); startService(serviceIntent); } break; // If access point was disabled case WIFI_AP_STATE_DISABLING: case WIFI_AP_STATE_DISABLED: case WifiManager.WIFI_STATE_DISABLING: case WifiManager.WIFI_STATE_DISABLED: if (apRunning) { unregisterReceiver(apReceiver); apRunning = false; for(StateChangedListener listener : listeners) { listener.apDisabled(autoApStartup); } Intent apDownIntent = new Intent(Constants.BROADCAST_INTENT_AP); apDownIntent.putExtra(Constants.INTENT_AP_EXTRA_STATE, false); sendBroadcast(apDownIntent); /* * If server is running, stop it * AP might have been stopped by external (user or app) */ if(PirateBoxService.isRunning()) { service.stopSelf(); } } break; } } } }; @Override public void onCreate() { super.onCreate(); preferences = PreferenceManager.getDefaultSharedPreferences(this); service = this; netUtil = new NetworkUtil(this); shellUtil = new ShellUtil(); // Save original WiFi state, mobile data state and access point configuration orgWifiState = netUtil.isWifiEnabled(); //orgMobileDataState = netUtil.getMobileDataEnabled(); // Not needed orgApConfig = netUtil.getWifiApConfiguration(); /* * Individual settings. */ init(); registerServiceListener(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { autoApStartup = preferences.getBoolean(Constants.PREF_AUTO_AP_STARTUP, true); configureAp = preferences.getBoolean(Constants.PREF_CONFIGURE_AP, true); emulateDroopy = preferences.getBoolean(Constants.PREF_EMULATE_DROOPY, true); externalServer = preferences.getBoolean(Constants.PREF_USE_EXTERNAL_SERVER, false); // If starting the access point is handled by the user if(!autoApStartup) { apRunning = true; for(StateChangedListener listener : listeners) { listener.apEnabled(autoApStartup); } if(!externalServer) { Log.d(TAG, "Starting service..."); return super.onStartCommand(intent, flags, startId); } else { handleExternalServerStart(); return START_NOT_STICKY; } } if(apRunning) { if(!externalServer) { Log.d(TAG, "Starting service..."); return super.onStartCommand(intent, flags, startId); } else { handleExternalServerStart(); return START_NOT_STICKY; } } else { Log.d(TAG, "Starting AccessPoint..."); WrapResult wrapResult = netUtil.wrapDnsmasq(NetworkUtil.getApIp(this)); // Inform about wrap result for(StateChangedListener listener : listeners) { listener.dnsMasqWrapped(wrapResult); } startAp(); return START_STICKY; } } @Override public WakeLock getWakeLock() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG); return wl; } @Override public WifiLock getWifiLock() { return super.getWifiLock(); } @Override public void onDestroy() { if(autoApStartup) { stopAp(); } else { apRunning = false; for(StateChangedListener listener : listeners) { listener.apDisabled(autoApStartup); } } teardownNetworking(); if(!externalServer) { super.onDestroy(); } else { handleExternalServerStop(); } } /** * Starts up the access point */ public void startAp() { startingUp = true; netUtil.setWifiEnabled(false); //netUtil.setMobileDataEnabled(true); // Not needed netUtil.setWifiApEnabled(null, false); IntentFilter filter = new IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"); registerReceiver(apReceiver, filter); String ssid = preferences.getString(Constants.PREF_SSID_NAME, service.getResources().getString(R.string.pref_ssid_name_default)); // If AP should be configured, create an open AP configuration, otherwise use existing. netUtil.setWifiApEnabled(configureAp ? netUtil.createOpenAp(ssid) : orgApConfig, true); } /** * Stops the access point */ public void stopAp() { boolean restoreWiFi = true; // If AP has been stop from external (user/app), do not restore WiFi state if(!apRunning) { restoreWiFi = false; Log.i(TAG, "AP stopped from external ... do not restore WiFi"); } netUtil.setWifiApEnabled(null, false); if(restoreWiFi) { netUtil.setWifiEnabled(orgWifiState); } //netUtil.setMobileDataEnabled(orgMobileDataState); // Not needed } /* * Service options are: * TAG = Tag name for message logging. * startOnBoot = Indicates if service has been started on boot. * isRuntime = If set to true this will only allow local connections. * serverConfig = Path to server configuration directory. * pawHome = PAW installation directory. * useWakeLock = Switch wakelock on or off. * hideNotificationIcon = Set to true if no notifications should be shown. * execAutostartScripts = Set to true if scripts inside the autostart directory should be executed onstartup. * showUrlInNotification = Set to true if URL should be shown in notification. * notificationTitle = The notification title. * notificationMessage = The notification message. * appName = Application name" * activityClass = Activity class name. * notificationDrawableId = ID of the notification icon to display. */ private void init() { TAG = "PirateBoxService"; startedOnBoot = false; isRuntime = false; serverConfig = Constants.getInstallDir(this) + "/conf/server.xml"; pawHome = Constants.getInstallDir(this) + "/"; useWakeLock = preferences.getBoolean(Constants.PREF_KEEP_DEVICE_ON, true); useWifiLock = preferences.getBoolean(Constants.PREF_KEEP_DEVICE_ON, true); hideNotificationIcon = false; execAutostartScripts = false; showUrlInNotification = false; notificationTitle = getString(R.string.app_name); notificationMessage = getString(R.string.notification_message); appName = getString(R.string.app_name); activityClass = "de.fun2code.android.piratebox.PirateBoxActivity"; notificationDrawableId = R.drawable.ic_notification; // Set default mayPost value PawServer.DEFAULT_MAX_POST = Constants.DEFAULT_MAX_POST; /* * Check if maxPost setting is valid */ String maxPost = ServerConfigUtil.getServerSetting("maxPost", this); if(maxPost.length() > 0) { try { Long.decode(maxPost).longValue(); } catch(NumberFormatException e) { String msg = new MessageFormat( getString(R.string.msg_max_post_invalid)).format(new Object[] { Formatter.formatFileSize(this, Constants.DEFAULT_MAX_POST) }); Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } } Log.i(TAG, "Home directory: " + Constants.getInstallDir(this)); } /** * Changes the dnsmasq configuration by killing the original dnsmasq * process and starting a new one which answers all DNS queries with the IP * of the access point. */ public void setupDnsmasq() { String apIp = NetworkUtil.getApIp(this); String baseIp = apIp.substring(0, apIp.lastIndexOf(".")); if (shellUtil.getProcessPid(NetworkUtil.DNSMASQ_BIN) != -1) { // Kill dnsmasqd shellUtil.killProcessByName(NetworkUtil.DNSMASQ_BIN); // Start new dnsmasqd String[] dnsmasqCmd = new String[] { NetworkUtil.DNSMASQ_BIN + " --no-resolv --no-poll --dhcp-range=" + baseIp +".2," + baseIp + ".254,1h --address=/#/" + apIp + " --pid-file=" + getFilesDir().getAbsolutePath() + "/dnsmasq.pid" }; shellUtil.execRootShell(dnsmasqCmd); } } /** * Redirects port 80 requests to the port of the running server * * @param action */ public void doRedirect(IpTablesAction action) { // Redirect port 80 netUtil.redirectPort(action, NetworkUtil.getApIp(this), NetworkUtil.PORT_HTTP, NetworkUtil.getServerPortNumber(this)); // Redirect port 8080 (Droopy support) if(emulateDroopy) { netUtil.redirectPort(action, NetworkUtil.getApIp(this), NetworkUtil.PORT_DROOPY, NetworkUtil.getServerPortNumber(this)); } for(StateChangedListener listener : listeners) { if(action == IpTablesAction.IP_TABLES_ADD) { listener.networkUp(); } else { listener.networkDown(); } } Intent intent = new Intent(Constants.BROADCAST_INTENT_NETWORK); intent.putExtra(Constants.INTENT_NETWORK_EXTRA_STATE, action == IpTablesAction.IP_TABLES_ADD ? true : false); sendBroadcast(intent); } /** * Stops the PirateBox networking setup */ public void teardownNetworking() { shellUtil.killProcessByName(NetworkUtil.DNSMASQ_BIN); // Set iptables FORWARD chain to ACCEPT to avoid Internet usage NetworkUtil.acceptChain(NetworkUtil.IPTABLES_CHAIN_FORWARD); doRedirect(IpTablesAction.IP_TABLES_DELETE); for(StateChangedListener listener : listeners) { listener.networkDown(); } networkRunning = false; } /** * Checks if the service is in startup phase * * @return {@code true} if the service is in startup phase, otherwise * {@code false} */ public static boolean isStartingUp() { return startingUp; } /** * Checks if the access point has been started * * @return {@code true} if the access point has been started, otherwise * {@code false} */ public static boolean isApRunning() { return apRunning; } /** * Checks if the PirateBox networking is set up * * @return {@code true} if the networking has been set up, otherwise * {@code false} */ public static boolean isNetworkRunning() { return networkRunning; } /** * Registers a StateChangedListener which will be informed if the * state of access point, networking or web service changes * * @param listener listener to register */ public static void registerChangeListener(StateChangedListener listener) { if(!listeners.contains(listener)) { listeners.add(listener); } } /** * Unregisters a StateChangedListener * * @param listener listener to unregister */ public static void unregisterChangeListener(StateChangedListener listener) { listeners.remove(listener); } @Override public void onServiceStart(boolean success) { for(StateChangedListener listener : listeners) { listener.serverUp(success); } Intent intent = new Intent(Constants.BROADCAST_INTENT_SERVER); intent.putExtra(Constants.INTENT_SERVER_EXTRA_STATE, true); sendBroadcast(intent); doRedirect(IpTablesAction.IP_TABLES_ADD); // Set iptables FORWARD chain to DROP NetworkUtil.dropChain(NetworkUtil.IPTABLES_CHAIN_FORWARD); networkRunning = true; startingUp = false; } @Override public void onServiceStop(boolean success) { for(StateChangedListener listener : listeners) { listener.serverDown(success); } Intent intent = new Intent(Constants.BROADCAST_INTENT_SERVER); intent.putExtra(Constants.INTENT_SERVER_EXTRA_STATE, false); sendBroadcast(intent); unregisterServiceListener(this); } /** * Handles the startup if an external server is used */ private void handleExternalServerStart() { externalServerRunning = true; // If external server, skip server start and inform listeners onServiceStart(true); // Show notification String titelExtension = " (" + getString(R.string.msg_external_server) + ")"; displayNotification(EXTERNAL_SERVER_NOTIFICATION_ID, notificationDrawableId, appName + titelExtension, notificationTitle + titelExtension, notificationMessage); // Widget Broadcast Intent msg = new Intent(PirateBoxWidget.WIDGET_INTENT_UPDATE); sendBroadcast(msg); } /** * Handles the stopping of the external server */ private void handleExternalServerStop() { // If external server call listeners directly for (StateChangedListener listener : listeners) { listener.serverDown(true); } externalServerRunning = false; removeNotification(EXTERNAL_SERVER_NOTIFICATION_ID); // Widget Broadcast Intent msg = new Intent(PirateBoxWidget.WIDGET_INTENT_UPDATE); sendBroadcast(msg); } /** * Displays the server status notification * * @param notificationId notification ID to use * @param notificationDrawableId Drawable ID to use * @param appName application name * @param notificationTitle the notification title * @param notificationMessage the notification message */ @SuppressWarnings("deprecation") private void displayNotification(int notificationId, int notificationDrawableId, String appName, String notificationTitle, String notificationMessage) { try { int icon = notificationDrawableId; long when = System.currentTimeMillis(); Notification notification = new Notification(icon, notificationTitle, when); Context context = getApplicationContext(); Intent notificationIntent = new Intent(this, Class.forName(activityClass)); notificationIntent.setAction("android.intent.action.MAIN"); notificationIntent.addCategory("android.intent.category.LAUNCHER"); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); notification.setLatestEventInfo(context, appName, notificationMessage, contentIntent); notification.flags |= Notification.FLAG_NO_CLEAR; notification.flags |= Notification.FLAG_ONGOING_EVENT; //notificationManager.notify(NOTIFICATION_ID, notification); ServiceCompat sc = new ServiceCompat(this); sc.startForegroundCompat(notificationId, notification); } catch (Resources.NotFoundException e) { // shouldn't happen } catch (ClassNotFoundException ec) { Log.e(TAG, "Could not create notification: " + ec.getMessage()); } } /** * Removes the status notification * * @param notificationId notification ID to use */ private void removeNotification(int notificationId) { if (hideNotificationIcon) { return; } //notificationManager.cancel(NOTIFICATION_ID); ServiceCompat sc = new ServiceCompat(this); sc.stopForegroundCompat(notificationId); } /** * Indicates if PirateBox is running * It is running, if PAW server is running or if an external server * has been started. * * @return {@code true} if the PirateBox is running, otherwise {@code false} */ public static boolean isRunning() { return PawServerService.isRunning() || externalServerRunning; } }