【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语言写代码,

想试试却没有安装,可参考以下文章开始安装

文章要讲的项目,是以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,做好一个主页面,修改后如下图
图1

在页面逻辑下,写好初始化代码,如下:

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(),这里面去调用模块实现webServerstart()方法,开启服务,

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);  
    }  
    //...
}

从上面看出,调用了实例webServerstop()方法,就停止了服务器

聪明的你也许留意到了,服务器是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_namesqlite的操作数据语句,
对做过数据处理的人来说,这是基本的工作,跟操作数据表格文档很相似。

获取权限

运行的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>

运行测试

编译顺利的话,就可以安装到手机上,运行时效果如下
动画12.gif

动图里面展示的二维码没毛病,只是在这里不便展示罢了~

看了动图的操作步骤,首次运行需要设置好资源文件夹位置,该位置下文件夹分别解释如下:

  • image - 存放图片的文件夹
  • audio - 存放音乐的文件夹
  • video - 存放视频的文件夹
  • book - 存放电子书的文件夹
  • other - 其它,不知道如何分类的文件就暂时放在这里

必须有这些文件夹,然后放一些资源文件存在里面,

当用手机扫码访问,或打开浏览器输入IP地址访问时,效果如下:
图片描述

动图时长超了,不得不压缩降低画质才能上传成功;

看完动图,在线浏览的文件不用下载就可以打开并播放,没有像某些网盘一样有限速烦恼,现在是不是有点心动了呢。

文章到此结束,感谢您的耐心阅读。

项目源码

项目源码已整理完毕,可直接在下方入口获取,你可根据实际需求决定是否下载使用。

当你亲自测试时,浏览文件打开时可能会又有响应慢的情况,

  • 这可能与手机性能或网络环境有关,手机的运行速度和处理能力直接影响文件打开的响应时间;
  • 连接的WIFI路由器网速不足可能导致文件加载缓慢。路由器性能决定了局域网内的数据传输效率,与外部宽带网络无直接关联;
  • 建议更换更高性能的手机,可以提升文件处理速度,更换好的路由器设备能够优化局域网内的网络传输质量。

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TA远方

谢谢!收到你的爱╮(╯▽╰)╭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值