FTP是基于FTP协议来实现文件的管理,理论上只要将协议逐个实现,就可以实现一个FTP的服务端了,但需要一些时间,而且还是个体力活。现在有了SwiFTP的开源库,只要对其稍加改造,就可以将手机快速变成一个FTP的服务器。这里提供一个SwiFTP的下载地址https://github.com/sparkleDai/swiftp。
我们先来看看SwiFTP源码中实现的效果图:
前一个图是FTP的配置,后一个图是FTP服务器控制。这两个画面,一看就是一嘛黑的,老外貌似比较喜欢这种风格。下面是修改后的效果图。
修改后的FTP服务端只有一个启动/停止的按钮,其他的都采用默认设置。下面我们来看具体的修改步骤。
1、跳过配置画面
SwiFTP一开启就会跳到配置画面,经查代码,发现是在ServerControlActivity的OnResume中跳转过来的,所以只要想办法跳过去就可以了。要跳过去,有几种方式,最直接的就是屏蔽掉。不过,考虑到原来有配置画面,这里可能需要留一个口来增加设置,所以这里我增加了一个配置函数,将所需要的配置项配置成了默认项,代码如下。(我将ServerControlActivity改成了MainActivity)
MainActivity.java
protected void onResume() { super.onResume(); // If the required preferences are not present, launch the configuration // Activity. configSetting(); if (!requiredSettingsDefined()) { launchCONFIG_KEYS(); } UiUpdater.registerClient(handler); updateUi(); // Register to receive wifi status broadcasts myLog.l(Log.DEBUG, "Registered for wifi updates"); this.registerReceiver(wifiReceiver, new IntentFilter( WifiManager.WIFI_STATE_CHANGED_ACTION)); } private void configSetting() { // Validation was successful, save the settings object SharedPreferences settings = getSharedPreferences( Defaults.getSettingsName(), Defaults.getSettingsMode()); SharedPreferences.Editor editor = settings.edit(); editor.putString(CONFIG_KEYS.USERNAME, Defaults.username); editor.putString(CONFIG_KEYS.PASSWORD, Defaults.password); editor.putInt(CONFIG_KEYS.PORTNUM, 2121); editor.putString(CONFIG_KEYS.CHROOTDIR, Defaults.chrootDir); editor.putBoolean(CONFIG_KEYS.ACCEPT_WIFI, Defaults.acceptWifi); editor.putBoolean(CONFIG_KEYS.ACCEPT_NET, Defaults.acceptNet); editor.putBoolean(CONFIG_KEYS.STAY_AWAKE, Defaults.stayAwake); editor.putBoolean(CONFIG_KEYS.IS_ANONYMOUS, Defaults.isAnonymous); editor.commit(); }
package com.sparkle.ftp; public class CONFIG_KEYS { public final static String USERNAME = "username"; public final static String PASSWORD = "password"; public final static String PORTNUM = "portNum"; public final static String CHROOTDIR = "chrootDir"; public final static String ACCEPT_WIFI = "allowWifi"; public final static String ACCEPT_NET = "allowNet"; public final static String STAY_AWAKE = "stayAwake"; public final static String IS_ANONYMOUS="isAnonymous"; }
Defaults.java部分代码
protected static int inputBufferSize = 256; protected static int dataChunkSize = 65536; // do file I/O in 64k chunks protected static int sessionMonitorScrollBack = 10; protected static int serverLogScrollBack = 10; protected static int uiLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; protected static int consoleLogLevel = Defaults.release ? Log.INFO : Log.DEBUG; protected static String settingsName = "FTP"; public static String username = "Anonynous"; public static String password = ""; protected static int portNumber = 2121; // protected static int ipRetrievalAttempts = 5; public static final int tcpConnectionBacklog = 5; public static final String chrootDir = Environment.getExternalStorageDirectory().getAbsolutePath(); public static final boolean acceptWifi = true; public static final boolean acceptNet = false; // don't incur bandwidth charges public static final boolean stayAwake = false; public static final boolean isAnonymous=true; public static final int REMOTE_PROXY_PORT = 2222; public static final String STRING_ENCODING = "UTF-8"; public static final int SO_TIMEOUT_MS = 30000; // socket timeout millis
注:
(1)、Activity的生命周期是OnCreate->OnStart->OnResume->OnPause->OnStop->OnDestory。在activity1跳转到另一个activity2后,如果跳转时activity1没有finish掉,那么activity2关闭跳后,activity1会从activity堆栈中重新唤醒,也就是会调用OnResume。所以在配置的activity中,如果没有配置,当点cancel,配置的activity虽然被关闭了,但是当回到服务控制的activity后,又激活了OnResume,然后判断配置的情况,如果不符合,又会启动配置的activity。所以会发现,如果没有配置,即使点cancel也没有作用,不知道的还以为中镖了。
(2)、SharedPreference是共享数据的一种方式,可以实现跨activity的数据共享,是一个轻量的存储方式,本质上是一个xml的key-value对。对其修改数据时,需要请求edit,然后修改数据,最后还要commit。这个和提交代码到git/svn等类似。
(3)、CONFIG_KEYS是将原来配置的activity中的一些key写到了这个类中。
(4)、Defaults中增加了username、password、isAnonymous。其中isAnonymous是为了实现FTP的匿名访问而增设的一个配置项。
(5)、由于手机内存中的文件不一定都能访问,所以FTP默认的目录设置到了SD卡中。在不同的设备中,对于SD卡的路径有所不同,所以采用了系统自带的函数来实现,即chrootDir = Environment.getExternalStorageDirectory().getAbsolutePath()。
2、实现匿名访问
现在将服务端跑起来后,已经可以正常访问了,不过每次访问的时候都会弹出用户名和密码的输入框,让人很烦,所以就想着法子的屏蔽掉。既然会弹出这个框,程序中肯定会有对应的判断项,经过查找,发现是在FTP中的PASS命令中实现的。所以对CmdPASS.java作了些修改,修改部分的代码如下。
public void run() { // User must have already executed a USER command to // populate the Account object's username myLog.l(Log.DEBUG, "Executing PASS"); Context ctx = Globals.getContext(); SharedPreferences settings = ctx.getSharedPreferences( Defaults.getSettingsName(), Defaults.getSettingsMode()); boolean isAnonymous=settings.getBoolean(CONFIG_KEYS.IS_ANONYMOUS,false); if(isAnonymous) { sessionThread.writeString("230 Access granted "); myLog.l(Log.INFO, "Anonymous visit!"); sessionThread.authAttempt(true); return; } if(ctx == null) { // This will probably never happen, since the global // context is configured by the Service myLog.l(Log.ERROR, "No global context in PASS "); } String attemptPassword = getParameter(input, true); // silent String attemptUsername = sessionThread.account.getUsername(); if(attemptUsername == null) { sessionThread.writeString("503 Must send USER first "); return; } String username = settings.getString("username", null); String password = settings.getString("password", null); if(username == null || password == null) { myLog.l(Log.ERROR, "Username or password misconfigured"); sessionThread.writeString("500 Internal error during authentication"); } else if(username.equals(attemptUsername) && password.equals(attemptPassword)) { sessionThread.writeString("230 Access granted "); myLog.l(Log.INFO, "User " + username + " password verified"); sessionThread.authAttempt(true); } else { try { // If the login failed, sleep for one second to foil // brute force attacks Thread.sleep(1000); } catch(InterruptedException e) {} myLog.l(Log.INFO, "Failed authentication"); sessionThread.writeString("530 Login incorrect. "); sessionThread.authAttempt(false); } }
注:
(1)、通过SharedPreferences获取配置信息,判断是否配置为匿名访问,如果是,则不再校验用户名和密码。
(2)、sessionThread.writeString("230 Access granted
")和sessionThread.authAttempt(true)是授权是否允许的部分,true是允许,false是不允许。
3、界面的实现
对于界面,重新写了一个,下面是界面的代码。
对于界面,重新写了一个,下面是界面的代码。
MainActivity.java
package com.sparkle.ftp; import java.net.InetAddress; import android.annotation.SuppressLint; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private Button _startStop_Button=null; private TextView _ip_TextView=null; private ImageView _ftpStatus_imageView=null; protected MyLog myLog = new MyLog(this.getClass().getName()); protected Context activityContext = this; @SuppressLint("HandlerLeak") public Handler handler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 0: // We are being told to do a UI update // If more than one UI update is queued up, we only need to do // one. removeMessages(0); updateUi(); break; case 1: // We are being told to display an error message removeMessages(1); } } }; public MainActivity() { } /** Called with the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Request no title bar on our window //requestWindowFeature(Window.FEATURE_NO_TITLE); // Set the application-wide context global, if not already set Context myContext = Globals.getContext(); if (myContext == null) { myContext = getApplicationContext(); if (myContext == null) { throw new NullPointerException("Null context!?!?!?"); } Globals.setContext(myContext); } // Inflate our UI from its XML layout description. setContentView(R.layout.main_activity); _ip_TextView = (TextView) findViewById(R.id.ip_address); _ftpStatus_imageView=(ImageView)findViewById(R.id.ftp_status); _startStop_Button = (Button) findViewById(R.id.start_stop_button); _startStop_Button.setOnClickListener(startStopListener); } /** * Whenever we regain focus, we should update the button text depending on * the state of the server service. */ protected void onStart() { super.onStart(); UiUpdater.registerClient(handler); updateUi(); } protected void onResume() { super.onResume(); // If the required preferences are not present, launch the configuration // Activity. configSetting(); if (!requiredSettingsDefined()) { launchCONFIG_KEYS(); } UiUpdater.registerClient(handler); updateUi(); // Register to receive wifi status broadcasts myLog.l(Log.DEBUG, "Registered for wifi updates"); this.registerReceiver(wifiReceiver, new IntentFilter( WifiManager.WIFI_STATE_CHANGED_ACTION)); } /* * Whenever we lose focus, we must unregister from UI update messages from * the FTPServerService, because we may be deallocated. */ protected void onPause() { super.onPause(); UiUpdater.unregisterClient(handler); myLog.l(Log.DEBUG, "Unregistered for wifi updates"); this.unregisterReceiver(wifiReceiver); } protected void onStop() { super.onStop(); UiUpdater.unregisterClient(handler); } protected void onDestroy() { super.onDestroy(); UiUpdater.unregisterClient(handler); } private void configSetting() { // Validation was successful, save the settings object SharedPreferences settings = getSharedPreferences( Defaults.getSettingsName(), Defaults.getSettingsMode()); SharedPreferences.Editor editor = settings.edit(); editor.putString(CONFIG_KEYS.USERNAME, Defaults.username); editor.putString(CONFIG_KEYS.PASSWORD, Defaults.password); editor.putInt(CONFIG_KEYS.PORTNUM, 2121); editor.putString(CONFIG_KEYS.CHROOTDIR, Defaults.chrootDir); editor.putBoolean(CONFIG_KEYS.ACCEPT_WIFI, Defaults.acceptWifi); editor.putBoolean(CONFIG_KEYS.ACCEPT_NET, Defaults.acceptNet); editor.putBoolean(CONFIG_KEYS.STAY_AWAKE, Defaults.stayAwake); editor.putBoolean(CONFIG_KEYS.IS_ANONYMOUS, Defaults.isAnonymous); editor.commit(); } /** * This will be called by the static UiUpdater whenever the service has * changed state in a way that requires us to update our UI. * * We can't use any myLog.l() calls in this function, because that will * trigger an endless loop of UI updates. */ public void updateUi() { WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); int wifiState = wifiMgr.getWifiState(); myLog.l(Log.DEBUG, "Updating UI", true); if (FTPServerService.isRunning()) { myLog.l(Log.DEBUG, "updateUi: server is running", true); // Put correct text in start/stop button _startStop_Button.setText(R.string.stop_server); // Fill in wifi status and address InetAddress address = FTPServerService.getWifiIp(); if (address != null) { _ip_TextView.setText("ftp://" + address.getHostAddress() + ":" + FTPServerService.getPort() + "/"); _ftpStatus_imageView.setBackgroundDrawable(getResources().getDrawable(R.drawable.ftp_on)); } else { myLog.l(Log.VERBOSE, "Null address from getServerAddress()", true); _ip_TextView.setText(R.string.cant_get_url); _ftpStatus_imageView.setBackgroundDrawable(getResources().getDrawable(R.drawable.ftp_off)); } } else { myLog.l(Log.DEBUG, "updateUi: server is not running", true); // Update the start/stop button to show the correct text _startStop_Button.setText(R.string.start_server); _ip_TextView.setText(R.string.no_url_yet); _startStop_Button.setText(R.string.start_server); _ftpStatus_imageView.setBackgroundDrawable(getResources().getDrawable(R.drawable.ftp_off)); } } /** * Called when your activity's options menu needs to be created. */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); return true; } /** * Called right before your activity's option menu is displayed. */ @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); return true; } /** * Called when a menu item is selected. */ @Override public boolean onOptionsItemSelected(MenuItem item) { /* * switch (item.getItemId()) { case BACK_ID: finish(); return true; case * CLEAR_ID: mEditor.setText(""); return true; } */ return super.onOptionsItemSelected(item); } OnClickListener startStopListener = new OnClickListener() { public void onClick(View v) { Context context = getApplicationContext(); Intent intent = new Intent(context, FTPServerService.class); /* * In order to choose whether to stop or start the server, we check * the text on the button to see which action the user was * expecting. */ String startString = getString(R.string.start_server); String stopString = getString(R.string.stop_server); String buttonText = _startStop_Button.getText().toString(); if (buttonText.equals(startString)) { /* The button had the "start server" text */ if (!FTPServerService.isRunning()) { warnIfNoExternalStorage(); context.startService(intent); } } else if (buttonText.equals(stopString)) { /* * The button had the "stop server" text. We stop the server * now. */ context.stopService(intent); } else { // Do nothing myLog.l(Log.ERROR, "Unrecognized start/stop text"); } } }; private void warnIfNoExternalStorage() { String storageState = Environment.getExternalStorageState(); if (!storageState.equals(Environment.MEDIA_MOUNTED)) { myLog.i("Warning due to storage state " + storageState); Toast toast = Toast.makeText(this, R.string.storage_warning, Toast.LENGTH_LONG); toast.setGravity(Gravity.CENTER, 0, 0); toast.show(); } } OnClickListener addUserListener = new OnClickListener() { public void onClick(View v) { myLog.l(Log.INFO, "Add user stub"); } }; OnClickListener manageUsersListener = new OnClickListener() { public void onClick(View v) { myLog.l(Log.INFO, "Manage users stub"); } }; OnClickListener serverOptionsListener = new OnClickListener() { public void onClick(View v) { myLog.l(Log.INFO, "Server options stub"); } }; DialogInterface.OnClickListener ignoreDialogListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { } }; /** * A call-back for when the user presses the "setup" button. */ OnClickListener setupListener = new OnClickListener() { public void onClick(View v) { launchCONFIG_KEYS(); } }; void launchCONFIG_KEYS() { if (!requiredSettingsDefined()) { Toast toast = Toast.makeText(this, R.string.must_config, Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, 0); toast.show(); } Intent intent = new Intent(activityContext, CONFIG_KEYS.class); startActivity(intent); } /** * A callback for when the user toggles the session monitor on or off */ OnClickListener sessionMonitorCheckBoxListener = new OnClickListener() { public void onClick(View v) { // Trigger a UI update message to our Activity UiUpdater.updateClients(); // updateUi(); } }; /** * A callback for when the user toggles the server log on or off */ OnClickListener serverLogCheckBoxListener = new OnClickListener() { public void onClick(View v) { // Trigger a UI update message to our Activity UiUpdater.updateClients(); // updateUi(); } }; BroadcastReceiver wifiReceiver = new BroadcastReceiver() { public void onReceive(Context ctx, Intent intent) { myLog.l(Log.DEBUG, "Wifi status broadcast received"); updateUi(); } }; boolean requiredSettingsDefined() { SharedPreferences settings = getSharedPreferences( Defaults.getSettingsName(), Defaults.getSettingsMode()); String username = settings.getString("username", null); String password = settings.getString("password", null); if (username == null || password == null) { return false; } else { return true; } } /** * Get the settings from the FTPServerService if it's running, otherwise * load the settings directly from persistent storage. */ SharedPreferences getSettings() { SharedPreferences settings = FTPServerService.getSettings(); if (settings != null) { return settings; } else { return this.getPreferences(MODE_PRIVATE); } } }
main_activity.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" tools:context=".MainActivity" > <LinearLayout android:id="@+id/content" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginTop="50dp" android:gravity="center" android:orientation="vertical" > <ImageView android:id="@+id/ftp_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/ftp_off" /> <TextView android:id="@+id/ip_address" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_url_yet" android:textColor="#000000" android:textSize="25sp" /> </LinearLayout> <LinearLayout android:id="@+id/bottom_panel" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="30dp" android:gravity="center" android:orientation="horizontal" > <Button android:id="@+id/start_stop_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/start_server" android:background="@drawable/button_bg" android:textSize="40sp" /> </LinearLayout> </RelativeLayout>
styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="ActionButton"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> <item name="android:textAppearance">@style/TextAppearance.ActionButton</item> </style> <style name="TextAppearance" parent="android:TextAppearance"></style> <style name="AppBaseTheme" parent="android:Theme.Light"></style> <style name="TextAppearance.ActionButton"> <item name="android:textStyle">italic</item> </style> <style name="AppTheme" parent="AppBaseTheme"> <item name="android:windowTitleBackgroundStyle">@style/CustomWindowTitleBackground</item> <item name="android:windowTitleStyle">@style/CustomWindowTitle</item> </style> <style name="CustomWindowTitleBackground"> <item name="android:background">@drawable/title_bg_blue</item> </style> <style name="CustomWindowTitle" parent="AppBaseTheme"> <item name="android:textAppearance">@style/CustomWindowTitleText</item> <item name="android:layout_gravity">center</item> </style> <style name="CustomWindowTitleText" parent="android:TextAppearance.WindowTitle"> <item name="android:textColor">#ffffff</item> <item name="android:textSize">14sp</item> <item name="android:textStyle">bold</item> </style> </resources>
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="name_version">电脑管理手机文件 1.1.1</string> <string name="app_name">电脑管理手机文件</string> <string name="start_server"><b>启动</b></string> <string name="stop_server"><b>停止</b></string> <string name="no_url_yet"></string> <string name="my_url_is">电脑上输入:</string> <string name="cant_get_url">无法获取IP</string> <string name="storage_warning">没有SD卡</string> <string name="must_config">FTP未配置</string> </resources>
FTPServerService.java
package com.sparkle.ftp; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.util.ArrayList; import java.util.List; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.IBinder; import android.os.PowerManager; import android.util.Log; 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 List<SessionThread> sessionThreads = new ArrayList<SessionThread>(); private static SharedPreferences settings = null; NotificationManager notificationMgr = null; PowerManager.WakeLock wakeLock; public FTPServerService() { } public IBinder onBind(Intent intent) { // We don't implement this functionality, so ignore it return null; } @Override 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); } } 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(); 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 = settings.getBoolean(CONFIG_KEYS.ACCEPT_NET, Defaults.acceptNet); acceptWifi = settings.getBoolean(CONFIG_KEYS.ACCEPT_WIFI, Defaults.acceptWifi); fullWake = settings.getBoolean(CONFIG_KEYS.STAY_AWAKE, Defaults.stayAwake); // The username, password, and chrootDir are just checked for sanity String username = settings.getString(CONFIG_KEYS.USERNAME, Defaults.username); String password = settings.getString(CONFIG_KEYS.PASSWORD, Defaults.password); String chrootDir = settings.getString(CONFIG_KEYS.CHROOTDIR, Defaults.chrootDir); validateBlock: { if(username == null || password == null) { myLog.l(Log.ERROR, "Username or password is invalid"); break validateBlock; } File chrootDirAsFile = new File(chrootDir); if(!chrootDirAsFile.isDirectory()) { myLog.l(Log.ERROR, "Chroot dir is invalid"); break validateBlock; } Globals.setChrootDir(chrootDirAsFile); Globals.setUsername(username); return true; } // We reach here if the settings were not sane return false; } // 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 // Get NotificationManager reference String ns = Context.NOTIFICATION_SERVICE; notificationMgr = (NotificationManager) getSystemService(ns); // Instantiate a Notification int icon = R.drawable.notification; long when = System.currentTimeMillis(); } private void clearNotification() { if(notificationMgr == null) { // Get NotificationManager reference String ns = Context.NOTIFICATION_SERVICE; notificationMgr = (NotificationManager) getSystemService(ns); } notificationMgr.cancelAll(); myLog.d("Cleared notification"); } 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 try { setupListener(); } catch (IOException e) { myLog.l(Log.WARN, "Error opening port, check your network connection."); // 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(); } } 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(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) { return true; } 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 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"); } static public SharedPreferences getSettings() { return settings; } }
注:
(1)、以上作修改部分的代码。
(2)、代码中使用到的图片没有贴上,有需要的可以自行补上。
(3)、去除了Proxy部分。
以上就是基于SwiFTP的实现。
转载请注明出处: