想要把闲置手机设备改造成像NAS,或Web文件服务器一样的功能,方便在家庭WIFI局域网内随时随地访问备份文件,或者学习资源,如何开发呢,接下来讲一讲实现过程。
首先,请看一段有趣的故事。
当我在街头偶遇那位心仪已久的女孩,她正站在一个二手手机摊前,手里握着一部略显陈旧的手机,眼神中带着一丝不舍与疑惑,向摊贩询问着:
“请问我这个闲置手机能卖多少钱?”
摊贩接过手机,尝试开机后,给出了一个略显吝啬的报价:
“50块。”
她闻言,眉头微蹙,显然对这个价格不太满意:
“这可是我以前花几千块买的,一直用得好好的,没想到现在只值这么点。”
我见状,心中暗自窃喜,这或许是我展现技术魅力的大好时机。我走近她,轻声建议道:
“其实,这部手机还有很多潜在的价值,没必要急着卖掉。”
她抬头看向我,眼中闪过一丝好奇:
“哦?还有什么用呢?”
我微笑着解释:
“你可以为它装上一些实用的APP,比如时钟APP,既省电又能当电子闹钟用;或者,利用家里的局域网,把它变成一个简易的文件服务器,存点视频、照片、电子书,随时随地都能访问,比买NAS服务器划算多了,还能省电费呢。再比如,还有答题服务器,对学习很有帮助,是个刷题的好工具。”
她听得入了神,眼中闪烁着兴奋的光芒:
“真的吗?你能帮我试试吗?如果成功了,我请你吃大餐!”
我欣然答应,心中早已盘算着如何大展身手。跟随她来到家中,我见到了她的母亲,并做了简单的自我介绍。在温馨的氛围中,我开始了我的“技术表演”。
我接过她递来的手机,仔细检查后发现,虽然系统版本较旧(Android 5.0),但硬件状况尚可,只需装上APP便能焕发新生。我向她解释道:
“这个手机系统有点旧了,装不了现代的App,不过别担心,我是程序员,可以自己开发APP给它装上。”
她闻言,眼中闪过一丝惊讶与敬佩:
“没想到你还是个程序员,技术一定很棒吧!”
我谦虚地笑了笑,随即向她借用电脑,准备现场开发。她迅速取来笔记本电脑,我迅速下载开发工具,搭建起基本环境,开始了我的“魔法”表演。
首先,我为她打造了一款简洁实用的时钟APP,借鉴了之前看过作者TA远方发布的一篇文章中的代码思路,很快便大功告成。我将APP安装到手机上,递给她体验。她试用后,满意地点点头:
“不错哦,挺实用的!”
接着,我趁热打铁,开始为她开发答题系统APP。虽然这次难度有所提升,但在我丰富的经验与不懈的努力下,参考作者TA远方的另一篇文章,最终还是成功将其呈现在她面前。她试用后,兴奋不已:
“这个太棒了,对我学习帮助很大!”
随着夜幕的降临,我们共进晚餐,聊得愈发投机。她突然提起了文件服务器的事情:
“你之前说的文件服务器,能实现吗?我想把学习资料都存进去,使用网盘不用VIP会员,那下载太慢了。”
我笑着点点头,信心满满地答应道:
“当然可以,这个对我来说只是小菜一碟。不过,具体实现还需要一些时间。等下次见面,我给你一个完整的解决方案。”
就这样,我们在愉快的氛围中结束了这次难忘的会面。
一个人走在回家的路上,心中暗自庆幸着,自言自语道:
”还好没出丑,总算出来了,之前做出来的都是参考TA远方作者发布的技术文章做出来的,不知道接下来的文件服务器要怎么做,回去得好好学习,争取早点做出来。“
然后,给自己鼓励道:
”为了心仪的女孩,加油吧,少年!“
而我们的故事,也因这次技术交流而悄然有了交情…
好了,故事告一段落,
接下来,让本作者讲解如何开发这个文件服务器,一起感受故事里学习生活中带来更多的便利。

准备好闲置Android 安卓手机,电脑,
还需要你会用Android Studio开发工具,会用Java语言写代码,
想试试却没有安装,可参考以下文章开始安装
文章要讲的项目,是以Web服务器项目开发的基础上调整而来,之后的更多细节可参考以下文章
注意:
开发此项目编译的APP安装包适配在安卓手机系统Android 5.0以上
打开已安装好的Android Studio开发工具,新建一个项目,例如 FileBrowser,
在新建项目的窗口上,注意选择 No Activity,只有这一项,才能开发旧系统的App,
接下来,选择对应的
- Language 选择 Java,
- Minimum SDK 选择 API21,也就是最小适配 Android 5.0 以上的App,
- Build configuration language 选择 Groovy DSL(build.gradle)
最后,等开发工具Build 创建项目完成。
完成后,
要新建一个页面,点中项目文件夹,点鼠标右键,按以下步骤选择
New → Activity → Empty Views Activity
由于第一个页面是应用程序入口,要默认勾选Launcher Activity,
这样,一个页面就自动建好了,看看项目路径app/src/main下的文件,
会发现多了两个文件:
- 一个 MainActivity.java - 写页面逻辑
- 一个 main_layou.xml - 写页面布局
页面布局
打开项目下的布局文件main_layout.xml,做好一个主页面,修改后如下图

在页面逻辑下,写好初始化代码,如下:
public class MainActivity extends AppCompatActivity {
//...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//...
//获取按钮
tvAddress = findViewById(R.id.textViewServerAddress);
btnStart = findViewById(R.id.buttonStartServer);
btnStop = findViewById(R.id.buttonStopServer);
btnOpen = findViewById(R.id.buttonOpenBrowser);
btnUpdate = findViewById(R.id.buttonUpload);
btnClear = findViewById(R.id.buttonClear);
tvSourcePath = findViewById(R.id.textView5);
//按钮点击事件
btnStart.setOnClickListener(s->startServer());
btnStop.setOnClickListener(s->stopServer());
btnOpen.setOnClickListener(s->openBrowser());
btnUpdate.setOnClickListener(s->openFolderPicker());
btnClear.setOnClickListener(s->clearOrderData());
//...
// 初始化显示
updateUI();
// 检查权限
checkStorageAccess();
checkPermissions();
}
//...
}
接下来,主要是讲功能的实现,先准备两个模块,
模块就是功能,就好比轮子
只是想告诉你,轮子有人造好了,
就这样,你可以下载来用,
自己最好不要,重复重复造轮子,
若没有,令自己满意满意的轮子,
你可以,尝试造属于自己的轮子,
可是可是你不懂,写好轮子的感觉。
现在告诉你这两个轮子,也就是叫两个模块,
安装模块
这两个模块,分别是服务器和二维码,
服务器
这个服务器的模块名叫AndServer,安装这个模块还需要几个步骤,
打开app模块文件build.gradle,添加以下内容
plugins {
//...
id 'com.yanzhenjie.andserver'
}
dependencies {
//...
implementation 'com.yanzhenjie.andserver:api:2.1.12'
annotationProcessor 'com.yanzhenjie.andserver:processor:2.1.12'
}
还有一个文件build.gradle,跟上面的文件一样,但内容不一样,是放在项目的根目录下,
将其打开,添加以下内容
buildscript {
dependencies {
classpath 'com.yanzhenjie.andserver:plugin:2.1.12'
}
}
注意这个要在里面的内容
plugins { ... }前面的位置插入,否则编译会报错。
看过之前开发答题服务器文章的读者也许会问,为什么不用之前的模块插件NanoHTTPD做服务器?
作者用过,只是没有这个的好用吧,有时间自己试一下,觉得哪个好用就用哪个吧。
二维码
还有个二维码的模块名叫zxing,安装这个模块只要一个步骤,
打开app模块文件build.gradle,添加以下内容
dependencies {
//...
implementation 'com.google.zxing:core:3.5.2'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
}
实现功能
点击编辑器右上角的Sync Now,开发工具就会开始联网安装相关的模块,
等模块安装好了,接下来就实现功能
看看界面上按钮有什么,每个按钮都用到了几个功能,
需要一个个去实现:
更改
点击更改按钮,就会打开文件夹浏览窗口,用于选择文件夹位置,
点击按钮会调用一个方法,代码如下
private void openFolderPicker() {
//...
FolderPickerUtils.openFolderPicker(folderPickerLauncher);
}
其中方法openFolderPicker()会打开文件夹浏览窗口,这个窗口就用系统内置的,不用再单独做一个页面,
选择好文件夹后,窗口会调用folderPickerLauncher的一个方法,代码如下
private String selectedFolderPath = "";
private final ActivityResultLauncher<Intent> folderPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
String path = FolderPickerUtils.handleFolderPickerResult(
MainActivity.this, result.getResultCode(), result.getData());
setSelectedFolderPath(path);
}
}
);
将文件夹路径通过自定义方法setSelectedFolderPath(path)设置到selectedFolderPath,这样服务器就能获取到管理资源文件的文件夹路径。
开启服务器
点击开启服务器,就会开启一个后台服务,假设有个服务类WebServerService,代码如下
private WebServerService webServerService;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
WebServerService.LocalBinder binder = (WebServerService.LocalBinder) service;
webServerService = binder.getService();
//...
}
@Override
public void onServiceDisconnected(ComponentName name) {
//...
}
};
在之后的服务器开启时,会触发它里面这个方法onServiceConnected(),通过getService()返回实例化webServerService类,
这个服务类WebServerService需要自己实现,继承了服务,代码如下
public class WebServerService extends Service {
private AndroidWebServer webServer;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
//...
}
然后,重写它的方法onStartCommand(),这里面去调用模块实现webServer的start()方法,开启服务,
try {
webServer = new AndroidWebServer(SERVER_PORT, customFolderPath, this);
webServer.start();
// 启动成功
String ipAddress = NetworkUtils.getLocalIpAddress(this);
String serverUrl = "http://" + ipAddress + ":" + SERVER_PORT;
sendStatusUpdate(true, serverUrl, null);
} catch (Exception e) {
stopSelf();
// 启动失败
sendStatusUpdate(false, null, "启动失败: " + e.getMessage());
}
其中方法
sendStatusUpdate就是发送通知的,告诉用户当前服务器的状态
最后,别忘了在文件AndroidManifest.xml里添加service,内容如下
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<!-- ... -->
<application>
<activity>
<!-- ... -->
</activity>
<!-- 前台服务 -->
<service
android:name=".server.WebServerService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
当开启服务的按钮点击后,会调用一个方法,代码如下
private void startServer(){
//...
// 启动服务(会创建前台通知)
Intent serviceIntent = new Intent(this, WebServerService.class);
// 我们可以通过Intent传递文件夹路径
serviceIntent.putExtra("folder_path", selectedFolderPath);
startService(serviceIntent);
PreferenceUtils.setString(this, SET_SOURCE_PATH, selectedFolderPath);
showToast( "服务器开启");
}
可见,开启服务是这样的,通过
Intent传递,并带个参数selectedFolderPath,就是资源文件夹位置
当服务器成功开启,就要更新下一些按钮状态为可以点击,
还有展示二维码,这样可以扫码访问,操作方便。
之前讲的答题服务器的文章内有类似的实现,这里就不展开讲二维码怎么弄了,
这也是用到了模块名zxing里面的方法,实现生成二维码图片。
关闭服务器
点击关闭服务器,就会把之前开启的后台服务给关闭了,
点击的按钮会调用一个方法,代码如下
private void stopServer(){
if (isServiceBound && webServerService != null) {
// 停止服务
Intent serviceIntent = new Intent(this, WebServerService.class);
stopService(serviceIntent);
// 解绑
unbindService(serviceConnection);
isServiceBound = false;
showToast( "服务器关闭");
}
}
当解绑后,系统会调用实例webServerService的一个onDestroy()方法,代码如下
public void onDestroy() {
// 停止Web服务器
if (webServer != null) {
webServer.stop();
webServer = null;
sendStatusUpdate(false, null, null);
}
//...
}
从上面看出,调用了实例
webServer的stop()方法,就停止了服务器
聪明的你也许留意到了,服务器是webServer,是AndroidWebServer类,还需要自己实现,
这个实现就用到了AndServer模块,代码如下
public class AndroidWebServer {
private final Server server;
public AndroidWebServer(int port, String resourcePath, Context context){
//...
server = AndServer.webServer(context)
.port(port)
.timeout(10, TimeUnit.SECONDS)
.listener(new Server.ServerListener() {...})
.build();
}
public void start(){
if(!server.isRunning()) server.startup();
}
public void stop(){
if(server.isRunning()) server.shutdown();
}
public boolean isAlive(){
return server.isRunning();
}
}
这个没有继承模块AndServer,而是在里面实例化了模块对象Server,可控制它的开启和停止,
聪明的你也许会发现问题,服务器的请求控制器逻辑没有写,当然不是写在listener()里面,
它是没有写到一起的,这就写一个控制器ApiController类,代码如下
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody()
public String toMenuList(){
List<String> menus = new ArrayList<>();
menus.add(FolderName_Image);
menus.add(FolderName_Book);
menus.add(FolderName_Audio);
menus.add(FolderName_Video);
menus.add(FolderName_Other);
return String.format("[\"%s\"]", String.join("\",\"", menus));
}
@RequestMapping(method= RequestMethod.POST, path="/filelist/{dirname}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String toDirFileList(@PathVariable("dirname") String dirName, @RequestBody String jsonBody) throws Exception {
List<String> dirs = getRequestBodyList(jsonBody);
String path = dirs.isEmpty() ? "" : String.join("/", dirs);
// 安全验证
if (path.contains("..")) throw new Exception("error path "+path);
return toFileList(dirName, path);
}
//部分省略...
}
从代码中看出,这是处理两个请求:
- /api/list - 返回菜单列表,每个菜单项对应一个资源根目录文件夹;
- /api/filelist/{dirname} - 返回dirname目录下的文件以及文件夹列表;
学会Java开发,创建过
RESTful风格的控制器的同学,应该会发现它使用@RequestMapping()类似SpringMVC的注解;
这就是处理页面GET请求和POST请求的后台逻辑,
聪明的你又发现了问题,没有看到哪个代码会调用这个类ApiController,这个不用管,开发工具会自动调用它,这就是AndServer模块专为懒人开发者设计的;
对了,还有服务器配置要写下,再写一个类WebServerConfig,代码如下:
public class WebServerConfig implements WebConfig {
@Override
public void onConfig(Context context, Delegate configurator) {
// 配置静态资源路径
configurator.addWebsite(new CustomAssetsWebsite(context, "/wwwroot/"));
}
}
可见,这又是一个懒人设计,没见到哪个代码在调用它,
其中wwwroot是一个文件夹,类似前端托管,这里存放uniapp项目生成的H5页面文件(单独放一个自己写的index.html文件也行),
这个文件夹就放在项目目录位置下app/src/main/assets,内置在APP里面,
如果你要研究和修改作者开发的uniapp项目,可参考 Web文件在线浏览播放器-uniapp-项目源码
打开浏览器
点击打开浏览器,就会打开系统自带的浏览器,直接访问文件服务器的H5页面,
点击的按钮会调用一个方法,代码如下
private void openBrowser(){
String url = webServerService.getServerUrl();
if (url==null || url.isEmpty()) {
showToast( "无法获取网络IP地址");
} else {
// 打开浏览器访问Web服务器
LinkUtils.openUrl(this, url);
}
}
由于H5页面是用
uniapp项目做来编译出来的,旧手机(包括Android 5.0以下)的自带浏览器无法正常加载H5页面,用电脑或者最新的Android 8以上的系统浏览器就能正常打开访问
清空数据
点击清空数据这个不是那么重要的功能,它是用于清空H5页面POST请求服务器后台处理插入的数据,
实现过程是怎样的呢,
看看按钮点击后,会调用一个方法clearOrderData(),代码如下
private void clearOrderData() {
SQLiteDataHelper helper = new SQLiteDataHelper(this);
SQLiteDatabase db = helper.getWritableDatabase();
helper.onUpgrade(db, 1, 1);
db.close();
showToast("已清空订单数据");
}
这是用到了数据库操作基础的功能,开发Android应用的程序员经常会用到它存取数据;
有一个类SQLiteDataHelper.class文件要自己去写,继承SQLiteOpenHelper,
然后重写它的一些方法,代码如下
public class SQLiteDataHelper extends SQLiteOpenHelper {
public SQLiteDataHelper(Context context) {
super(context, "data.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
//这里创建表...
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//这里更新表...
}
//自己实现其它的操作数据库方法...
}
清除数据的话,只需调用它的方法onUpgrade就可,
在这个方法里面,只执行清理表的数据,代码如下
db.execSQL("DROP TABLE IF EXISTS star_order");
是不是有点熟悉,
DROP TABLE IF EXISTS table_name是sqlite的操作数据语句,
对做过数据处理的人来说,这是基本的工作,跟操作数据表格文档很相似。
获取权限
运行的App首次获取权限是必不可少的:
- 需要访问网络状态,连接的WIFI;
- 需要通知,服务器工作的状态通知;
- 需要保持在后台运行的权限;
- 访问手机内部存储的权限,读取一些保存的文件;
在第一个页面MainActivity代码中处理初始化的时候,调用了两个方法,代码如下
// 检查权限
checkStorageAccess();
checkPermissions();
这就是检查存储和通知两个权限的,检查没有权限就去授权,自己能实现吧,
除了写授权逻辑代码,还要…
在配置文件AndroidManifest.xml下添加uses-permission ...,内容如下
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 对于Android 10+,需要添加这个权限来访问公共目录 -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 对于Android 11+,需要添加这个来管理所有文件 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<application
...
android:requestLegacyExternalStorage="true">
<!-- ... -->
</application>
</manifest>
运行测试
编译顺利的话,就可以安装到手机上,运行时效果如下

动图里面展示的二维码没毛病,只是在这里不便展示罢了~
看了动图的操作步骤,首次运行需要设置好资源文件夹位置,该位置下文件夹分别解释如下:
- image - 存放图片的文件夹
- audio - 存放音乐的文件夹
- video - 存放视频的文件夹
- book - 存放电子书的文件夹
- other - 其它,不知道如何分类的文件就暂时放在这里
必须有这些文件夹,然后放一些资源文件存在里面,
当用手机扫码访问,或打开浏览器输入IP地址访问时,效果如下:

动图时长超了,不得不压缩降低画质才能上传成功;
看完动图,在线浏览的文件不用下载就可以打开并播放,没有像某些网盘一样有限速烦恼,现在是不是有点心动了呢。
文章到此结束,感谢您的耐心阅读。
项目源码
项目源码已整理完毕,可直接在下方入口获取,你可根据实际需求决定是否下载使用。
当你亲自测试时,浏览文件打开时可能会又有响应慢的情况,
- 这可能与手机性能或网络环境有关,手机的运行速度和处理能力直接影响文件打开的响应时间;
- 连接的WIFI路由器网速不足可能导致文件加载缓慢。路由器性能决定了局域网内的数据传输效率,与外部宽带网络无直接关联;
- 建议更换更高性能的手机,可以提升文件处理速度,更换好的路由器设备能够优化局域网内的网络传输质量。

869

被折叠的 条评论
为什么被折叠?



