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;
}
}