/*
Copyright 2009 David Revell
This file is part of SwiFTP.
SwiFTP 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.
SwiFTP 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 SwiFTP. If not, see <http://www.gnu.org/licenses/>.
*/
package net.micode.fileexplorer;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import org.swiftp.Defaults;
import org.swiftp.Globals;
import org.swiftp.MyLog;
import org.swiftp.ProxyConnector;
import org.swiftp.SessionThread;
import org.swiftp.TcpListener;
import org.swiftp.UiUpdater;
import org.swiftp.Util;
import net.micode.fileexplorer.R;
public class FTPServerService extends Service implements Runnable {
protected static Thread serverThread = null;
protected boolean shouldExit = false;
protected MyLog myLog = new MyLog(getClass().getName());
protected static MyLog staticLog = new MyLog(FTPServerService.class.getName());
public static final int BACKLOG = 21;
public static final int MAX_SESSIONS = 5;
public static final String WAKE_LOCK_TAG = "SwiFTP";
// protected ServerSocketChannel wifiSocket;
protected ServerSocket listenSocket;
protected static WifiLock wifiLock = null;
// protected static InetAddress serverAddress = null;
protected static List<String> sessionMonitor = new ArrayList<String>();
protected static List<String> serverLog = new ArrayList<String>();
protected static int uiLogLevel = Defaults.getUiLogLevel();
// The server thread will check this often to look for incoming
// connections. We are forced to use non-blocking accept() and polling
// because we cannot wait forever in accept() if we want to be able
// to receive an exit signal and cleanly exit.
public static final int WAKE_INTERVAL_MS = 1000; // milliseconds
protected static int port;
protected static boolean acceptWifi;
protected static boolean acceptNet;
protected static boolean fullWake;
private TcpListener wifiListener = null;
private ProxyConnector proxyConnector = null;
private List<SessionThread> sessionThreads = new ArrayList<SessionThread>();
private static SharedPreferences settings = null;
PowerManager.WakeLock wakeLock;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED) && isRunning()) {
stopSelf();
}
}
};
public FTPServerService() {
}
public IBinder onBind(Intent intent) {
// We don't implement this functionality, so ignore it
return null;
}
public void onCreate() {
myLog.l(Log.DEBUG, "SwiFTP server created");
// Set the application-wide context global, if not already set
Context myContext = Globals.getContext();
if (myContext == null) {
myContext = getApplicationContext();
if (myContext != null) {
Globals.setContext(myContext);
}
}
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
intentFilter.addDataScheme("file");
registerReceiver(mReceiver, intentFilter);
return;
}
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
shouldExit = false;
int attempts = 10;
// The previous server thread may still be cleaning up, wait for it
// to finish.
while (serverThread != null) {
myLog.l(Log.WARN, "Won't start, server thread exists");
if (attempts > 0) {
attempts--;
Util.sleepIgnoreInterupt(1000);
} else {
myLog.l(Log.ERROR, "Server thread already exists");
return;
}
}
myLog.l(Log.DEBUG, "Creating server thread");
serverThread = new Thread(this);
serverThread.start();
// todo: we should broadcast an intent to inform anyone who cares
}
public static boolean isRunning() {
// return true if and only if a server Thread is running
if (serverThread == null) {
staticLog.l(Log.DEBUG, "Server is not running (null serverThread)");
return false;
}
if (!serverThread.isAlive()) {
staticLog.l(Log.DEBUG, "serverThread non-null but !isAlive()");
} else {
staticLog.l(Log.DEBUG, "Server is alive");
}
return true;
}
public void onDestroy() {
myLog.l(Log.INFO, "onDestroy() Stopping server");
shouldExit = true;
if (serverThread == null) {
myLog.l(Log.WARN, "Stopping with null serverThread");
return;
} else {
serverThread.interrupt();
try {
serverThread.join(10000); // wait 10 sec for server thread to
// finish
} catch (InterruptedException e) {
}
if (serverThread.isAlive()) {
myLog.l(Log.WARN, "Server thread failed to exit");
// it may still exit eventually if we just leave the
// shouldExit flag set
} else {
myLog.d("serverThread join()ed ok");
serverThread = null;
}
}
try {
if (listenSocket != null) {
myLog.l(Log.INFO, "Closing listenSocket");
listenSocket.close();
}
} catch (IOException e) {
}
UiUpdater.updateClients();
if (wifiLock != null) {
wifiLock.release();
wifiLock = null;
}
clearNotification();
unregisterReceiver(mReceiver);
myLog.d("FTPServerService.onDestroy() finished");
}
private boolean loadSettings() {
myLog.l(Log.DEBUG, "Loading settings");
settings = getSharedPreferences(Defaults.getSettingsName(), Defaults.getSettingsMode());
port = settings.getInt("portNum", Defaults.portNumber);
if (port == 0) {
// If port number from settings is invalid, use the default
port = Defaults.portNumber;
}
myLog.l(Log.DEBUG, "Using port " + port);
acceptNet = false;
acceptWifi = true;
fullWake = false;
return true;
}
// This opens a listening socket on all interfaces.
void setupListener() throws IOException {
listenSocket = new ServerSocket();
listenSocket.setReuseAddress(true);
listenSocket.bind(new InetSocketAddress(port));
}
private void setupNotification() {
// http://developer.android.com/guide/topics/ui/notifiers/notifications.html
// Instantiate a Notification
int icon = R.drawable.notification;
CharSequence tickerText = getString(R.string.notif_server_starting);
long when = System.currentTimeMillis();
Notification notification = new Notification(icon, tickerText, when);
// Define Notification's message and Intent
CharSequence contentTitle = getString(R.string.notif_title);
CharSequence contentText = "";
InetAddress address = FTPServerService.getWifiIp();
if (address != null) {
String port = ":" + FTPServerService.getPort();
contentText = "ftp://" + address.getHostAddress() + (FTPServerService.getPort() == 21 ? "" : port);
}
Intent notificationIntent = new Intent(this, FileExplorerTabActivity.class);
notificationIntent.putExtra(GlobalConsts.INTENT_EXTRA_TAB, 2);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(getApplicationContext(), contentTitle, contentText, contentIntent);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
startForeground(123453, notification);
myLog.d("Notication setup done");
}
private void clearNotification() {
stopForeground(true);
myLog.d("Cleared notification");
}
private boolean safeSetupListener() {
try {
setupListener();
} catch (IOException e) {
myLog.l(Log.WARN, "Error opening port, check your network connection.");
return false;
}
return true;
}
public void run() {
// The UI will want to check the server status to update its
// start/stop server button
int consecutiveProxyStartFailures = 0;
long proxyStartMillis = 0;
UiUpdater.updateClients();
myLog.l(Log.DEBUG, "Server thread running");
// set our members according to user preferences
if (!loadSettings()) {
// loadSettings returns false if settings are not sane
cleanupAndStopService();
return;
}
// Initialization of wifi
if (acceptWifi) {
// If configured to accept connections via wifi, then set up the
// socket
int maxTry = 10;
int atmp = 0;
while (!safeSetupListener() && ++atmp < maxTry) {
port += 1;
}
if (atmp >= maxTry) {
// serverAddress = null;
cleanupAndStopService();
return;
}
takeWifiLock();
}
takeWakeLock();
myLog.l(Log.INFO, "SwiFTP server ready");
setupNotification();
// We should update the UI now that we have a socket open, so the UI
// can present the URL
UiUpdater.updateClients();
while (!shouldExit) {
if (acceptWifi) {
if (wifiListener != null) {
if (!wifiListener.isAlive()) {
myLog.l(Log.DEBUG, "Joining crashed wifiListener thread");
try {
wifiListener.join();
} catch (InterruptedException e) {
}
wifiListener = null;
}
}
if (wifiListener == null) {
// Either our wifi listener hasn't been created yet, or has
// crashed,
// so spawn it
wifiListener = new TcpListener(listenSocket, this);
wifiListener.start();
}
}
if (acceptNet) {
if (proxyConnector != null) {
if (!proxyConnector.isAlive()) {
myLog.l(Log.DEBUG, "Joining crashed proxy connector");
try {
proxyConnector.join();
} catch (InterruptedException e) {
}
proxyConnector = null;
long nowMillis = new Date().getTime();
// myLog.l(Log.DEBUG,
// "Now:"+nowMillis+" start:"+proxyStartMillis);
if (nowMillis - proxyStartMillis < 3000) {
// We assume that if the proxy thread crashed within
// 3
// seconds of starting, it was a startup or
// connection
// failure.
myLog.l(Log.DEBUG, "Incrementing proxy start failures");
consecutiveProxyStartFailures++;
} else {
// Otherwise assume the proxy started successfully
// and
// crashed later.
myLog.l(Log.DEBUG, "Resetting proxy start failures");
consecutiveProxyStartFailures = 0;
}
}
}
if (proxyConnector == null) {
long nowMillis = new Date().getTime();
boolean shouldStartListener = false;
// We want to restart the proxy listener without much delay
// for the first few attempts, but add a much longer delay
// if we consistently fail to connect.
if (consecutiveProxyStartFailures < 3 && (nowMillis - proxyStartMillis) > 5000) {
// Retry every 5 seconds for the first 3 tries
shouldStartListener = true;
} else if (nowMillis - proxyStartMillis > 30000) {
// After the first 3 tries, only retry once per 30 sec
shouldStartListener = true;
}
if (shouldStartListener) {
myLog.l(Log.DEBUG, "Spawning ProxyConnector");
proxyConnector = new ProxyConnector(this);
proxyConnector.start();
proxyStartMillis = nowMillis;
}
}
}
try {
// todo: think about using ServerSocket, and just closing
// the main socket to send an exit signal
Thread.sleep(WAKE_INTERVAL_MS);
} catch (InterruptedException e) {
myLog.l(Log.DEBUG, "Thread interrupted");
}
}
terminateAllSessions();
if (proxyConnector != null) {
proxyConnector.quit();
proxyConnector = null;
}
if (wifiListener != null) {
wifiListener.quit();
wifiListener = null;
}
shouldExit = false; // we handled the exit flag, so reset it to
// acknowledge
myLog.l(Log.DEBUG, "Exiting cleanly, returning from run()");
clearNotification();
releaseWakeLock();
releaseWifiLock();
}
private void terminateAllSessions() {
myLog.i("Terminating " + sessionThreads.size() + " session thread(s)");
synchronized (this) {
for (SessionThread sessionThread : sessionThreads) {
if (sessionThread != null) {
sessionThread.closeDataSocket();
sessionThread.closeSocket();
}
}
}
}
public void cleanupAndStopService() {
// Call the Android Service shutdown function
Context context = getApplicationContext();
Intent intent = new Intent(context, FTPServerService.class);
context.stopService(intent);
releaseWifiLock();
releaseWakeLock();
clearNotification();
}
private void takeWakeLock() {
if (wakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// Many (all?) devices seem to not properly honor a
// PARTIAL_WAKE_LOCK,
// which should prevent CPU throttling. This has been
// well-complained-about on android-developers.
// For these devices, we have a config option to force the phone
// into a
// full wake lock.
if (fullWake) {
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, WAKE_LOCK_TAG);
} else {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
}
wakeLock.setReferenceCounted(false);
}
myLog.d("Acquiring wake lock");
wakeLock.acquire();
}
private void releaseWakeLock() {
myLog.d("Releasing wake lock");
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
myLog.d("Finished releasing wake lock");
} else {
myLog.i("Couldn't release null wake lock");
}
}
private void takeWifiLock() {
myLog.d("Taking wifi lock");
if (wifiLock == null) {
WifiManager manager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiLock = manager.createWifiLock("SwiFTP");
wifiLock.setReferenceCounted(false);
}
wifiLock.acquire();
}
private void releaseWifiLock() {
myLog.d("Releasing wifi lock");
if (wifiLock != null) {
wifiLock.release();
wifiLock = null;
}
}
public void errorShutdown() {
myLog.l(Log.ERROR, "Service errorShutdown() called");
cleanupAndStopService();
}
/**
* Gets the IP address of the wifi connection.
*
* @return The integer IP address if wifi enabled, or null if not.
*/
public static InetAddress getWifiIp() {
Context myContext = Globals.getContext();
if (myContext == null) {
throw new NullPointerException("Global context is null");
}
WifiManager wifiMgr = (WifiManager) myContext.getSystemService(Context.WIFI_SERVICE);
if (isWifiEnabled()) {
int ipAsInt = wifiMgr.getConnectionInfo().getIpAddress();
if (ipAsInt == 0) {
return null;
} else {
return Util.intToInet(ipAsInt);
}
} else {
return null;
}
}
public static boolean isWifiEnabled() {
Context myContext = Globals.getContext();
if (myContext == null) {
throw new NullPointerException("Global context is null");
}
WifiManager wifiMgr = (WifiManager) myContext.getSystemService(Context.WIFI_SERVICE);
if (wifiMgr.getWifiState() == WifiManager.WIFI_STATE_ENABLED) {
ConnectivityManager connManager = (ConnectivityManager) myContext
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo wifiInfo = connManager
.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
return wifiInfo.isConnected();
} else {
return false;
}
}
public static List<String> getSessionMonitorContents() {
return new ArrayList<String>(sessionMonitor);
}
public static List<String> getServerLogContents() {
return new ArrayList<String>(serverLog);
}
public static void log(int msgLevel, String s) {
serverLog.add(s);
int maxSize = Defaults.getServerLogScrollBack();
while (serverLog.size() > maxSize) {
serverLog.remove(0);
}
// updateClients();
}
public static void updateClients() {
UiUpdater.updateClients();
}
public static void writeMonitor(boolean incoming, String s) {
}
// public static void writeMonitor(boolean incoming, String s) {
// if(incoming) {
// s = "> " + s;
// } else {
// s = "< " + s;
// }
// sessionMonitor.add(s.trim());
// int maxSize = Defaults.getSessionMonitorScrollBack();
// while(sessionMonitor.size() > maxSize) {
// sessionMonitor.remove(0);
// }
// updateClients();
// }
public static int getPort() {
return port;
}
public static void setPort(int port) {
FTPServerService.port = port;
}
/**
* The FTPServerService must know about all running session threads so they
* can be terminated on exit. Called when a new session is created.
*/
public void registerSessionThread(SessionThread newSession) {
// Before adding the new session thread, clean up any finished session
// threads that are present in the list.
// Since we're not allowed to modify the list while iterating over
// it, we construct a list in toBeRemoved of threads to remove
// later from the sessionThreads list.
synchronized (this) {
List<SessionThread> toBeRemoved = new ArrayList<SessionThread>();
for (SessionThread sessionThread : sessionThreads) {
if (!sessionThread.isAlive()) {
myLog.l(Log.DEBUG, "Cleaning up finished session...");
try {
sessionThread.join();
myLog.l(Log.DEBUG, "Thread joined");
toBeRemoved.add(sessionThread);
sessionThread.closeSocket(); // make sure socket closed
} catch (InterruptedException e) {
myLog.l(Log.DEBUG, "Interrupted while joining");
// We will try again in the next loop iteration
}
}
}
for (SessionThread removeThread : toBeRemoved) {
sessionThreads.remove(removeThread);
}
// Cleanup is complete. Now actually add the new thread to the list.
sessionThreads.add(newSession);
}
myLog.d("Registered session thread");
}
/** Get the ProxyConnector, may return null if proxying is disabled. */
public ProxyConnector getProxyConnector() {
return proxyConnector;
}
static public SharedPreferences getSettings() {
return settings;
}
}