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