一、前言
到目前为止,最新的Android系统已经来到了Android S,即Android 12,但是现在网络上针对文件读取并上传的文章,还是针对Android N 甚至更早的Android系统来适配的,按照这些文章去上传用户文件,必然会踩很多坑。
我在踩过这些大大小小的坑后,结合代码分几步说一下Android高版本应该如何上传用户的文件。
二、权限申请
自从Android O(Android 10)开始,Android系统已经开始用分区存储取代以前的应用存储模式。“一般”情况下,我们创建的应用只能在系统划分给我们的一块区域内存放和读取数据。
而读取一些公共目录的图片等文件不再需要申请权限,用户选择了文件,返回了Uri,我们的应用就能根据Uri来读取用户选择的文件。
三、打开图片选择界面
这里用的是最新的Google官方ResultAPI来打开图片选择器,这里推荐看一下郭霖的这篇文章:
Activity Result API详解https://mp.weixin.qq.com/s/C62WVau-AU0MH7S4Ix85ewhttps://mp.weixin.qq.com/s/C62WVau-AU0MH7S4Ix85ew与startActivityForResult 和 onActivityResult结合使用不同,Result API只需要两行代码就可以代码图片选择器,首选创建一个launcher实例:
val pictureLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { pic ->
pic?.also { picList.add(pic) }
}
(没错这实际是一行代码)
然后在Button的点击事件里面,启动这个launcher:
Button(
onClick = { pictureLauncher.launch("image/*") },
colors = buttonColors(backgroundColor = topBarColor)
) {
Text(text = "上传附件", style = TextStyle(color = Color.White))
}
上面的第一句里面,局部变量pic是一个被返回的文件Uri。
四、解析Uri,包装成RequestBody并上传
以前的文件,对待Uri都是用UriMatcher等工具类解析,或者在ContentResolver里面查询,然后拿到Uri对应的文件路径,然后包装成File,再放入RequestBody。
我其实是有些奇怪的,都2021年,怎么处理Uri还是这么麻烦,就想办法找了个捷径,直接把Uri包装成RequestBody。
因为RequestBody是个抽象类,所以这里要RequestBody的匿名子类,实现一些抽象方法,这里我直接写了个Uri的扩展方法:
fun Uri.asRequestBody(activity: Context) : RequestBody{
val ins = activity.contentResolver?.openInputStream(this)
return object : RequestBody(){
override fun contentType(): MediaType? = formDataMediaType.toMediaType()
override fun writeTo(sink: BufferedSink) {
try {
val source = ins?.source() ?: return
sink.writeAll(source)
source.close()
}catch (e : IOException)
{
throw e
}
}
}
}
然后写一个上传的接口:
@Multipart
@POST("uploadFile")
suspend fun uploadSuggestions(
@Part files : List<MultipartBody.Part>) : UploadResult
在上传按键的点击事件里面,把用户选择的所有文件的Uri转成RequestBody,然后上传:
val fileList = mutableListOf<MultipartBody.Part>()
picList.forEach {
val doc = DocumentFile.fromSingleUri(activity,it)
val file = MultipartBody.Part.createFormData("files",doc?.name,it.asRequestBody(activity))
fileList.add(file)
}
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
val message = kotlin.runCatching {
val result = localRoofRepo().uploadSuggestions(messageId,suggestion,fileList)
Log.d(TAG, "MessageContent: ${result.msg}")
}
withContext(Dispatchers.Main)
{
if (message.isSuccess) {
Toast.makeText(activity, "上传成功", Toast.LENGTH_LONG).show()
controller?.navigateUp()
}
else {
Toast.makeText(activity, "上传失败", Toast.LENGTH_LONG).show()
message.exceptionOrNull()?.printStackTrace()
}
}
}
上面使用的kotlin 协程来进行IO操作,并捕捉异常。
五、后端接受前端传过来的文件
后端使用的是 apache 的
commons-fileupload
框架进行文件接收的,在pom.xml中添加依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
在Controller里面添加一个接口:
@PostMapping("uploadFile")
@ResponseBody
fun uploadFileFromDevice(request: HttpServletRequest) : String
{
val iter = upload.getItemIterator(request)
while (iter.hasNext())
{
val file = iter.next()
if (file.isFormField){
logger.debug(file.fieldName)
logger.debug(file.toString())
}else{
try {
val inStream = file.openStream()
Files.copy(inStream,Paths.get(filePath,file.name))
inStream.close()
}catch (e : Exception)
{
e.printStackTrace()
return "{\"msg\":\"failed\"}"
}
}
}
return "{\"msg\":\"success\"}"
}
遍历前端发过来的Request,把文件放入filePath路径里面。