1、前言
最近做了安卓APP的导入和导出,导入需要先引导用户选择文件并读取,导出则需要确定文件保存位置和写入。导入做的比较快,但导出时遇到了安卓高版本系统限制访问应用的私有数据目录,导致导出后文件在文件管理器中找不到(可能是视图过滤了某些目录,我的能在文件APP中访问)或者访问受限(提示只能在电脑上访问),无法导入。经过搜索,终于解决了这个问题。在此记录下解决的办法。其中文件的读取和写入可以看我上一篇Andorid+Java使用Apache POI库实现doc、docx、xls、xlsx的读取和写入-优快云博客。
高版本导出图片:
2、导入和导出
2.1 配置权限
首先,确保应用有权限读取和写入外部存储器。在 AndroidManifest.xml
文件中声明权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
2.2 导入
2.2.1 启动文件选择器
使用Intent启动文件选择器,让用户选择文件:
//REQUEST_CODE_PICK_FILE 是一个自定义的整数常量,用于标识 startActivityForResult 方法中的请求,
//以便在 onActivityResult 方法中能够区分不同的请求。
private static final int REQUEST_CODE_PICK_FILE = 42;
// 创建一个Intent来选择文件
private void selectFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
//指定了要创建的文档的 MIME 类型为 text/plain,即纯文本文件txt
intent.setType("text/plain");
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
}
2.2.2 处理选择结果
重写 onActivityResult
来处理文件选择结果:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//当文件选择器的结果返回时,可以使用 REQUEST_CODE_PICK_FILE 这个请求代码来确认结果是由哪个请求发起的
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
// 处理选中的文件
handleSelectedFile(uri);
}
}
}
若是intent.setType("*/*"),即允许选择任何类型的文件时(也可以设置只能选择某些类型的文件),需要从uri中提取文件名,从而进行不同文件类型的读取。由于uri 类型在Android高低版本中类型不同,有content://URI和file://URI,可以用下面代码来提取文件名。注意判断方法返回值是否为null,防止报错空指针异常。
public static String getFileType(Uri uri) {
String type = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)){
if (cursor != null && cursor.moveToFirst()) {
type = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE));
}
}
} else if (uri.getScheme().equals("file")) {
String path = uri.getPath();
type = getContentResolver().getType(Uri.fromFile(new File(path)));
}
if (type != null) {
switch (type) {
case "text/plain":
return "txt";
case "application/msword":
return "doc";
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return "docx";
case "application/vnd.ms-excel":
return "xls";
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return "xlsx";
default:
return null;
}
}
return null;
}
2.2.3 读取文件内容
使用 ContentResolver
从选中的 Uri
读取文件内容:
private void handleSelectedFile(Uri uri) {
try {
//getContentResolver().openOutputStream(uri): 通过 ContentResolver 获取对所选文件的 OutputStream
InputStream inputStream = getContentResolver().openInputStream(uri);
// 读取文件内容
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
//关闭输出流
inputStream.close();
// 处理文件内容
String fileContents = stringBuilder.toString();
//将文件内容打印到日志上
Log.d("test", fileContents);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.3 导出
由于安卓高版本系统限制访问应用的私有数据目录,有两种解决方式:
一、使用设备的文件管理应用(如“文件”应用、“文件管理器”应用)手动浏览并选择文件,之后将文件复制或移动到 Downloads
或其他公共目录,再打开应用并使用导入的文件选择器进行访问。
二、引导用户选择目录或文件保存位置。
以下导出使用的是第二种方式:
2.3.1 适配不同Android版本
下面这段代码用于处理 Android 设备上的文件保存操作,考虑到不同的 Android 版本,能够适应不同版本的存储权限模型,以确保应用在各个 Android 版本上都能正确保存文件:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// For Android 10 and above,Android 10 (API 29) 及以上版本
//Intent 对象用于启动一个文档选择器,让用户选择文件保存的位置。
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
//使用 Intent.ACTION_CREATE_DOCUMENT 选择文件位置并修改文件名,创建一个新文件,只在 Android 10 及以上版本有效
intent.addCategory(Intent.CATEGORY_OPENABLE);
//Intent 配置了文件类型 (setType("text/plain"))
intent.setType("text/plain");
//Intent.EXTRA_TITLE 提供了一个默认文件名,用户可以更改这个文件名,系统会处理文件保存的位置
intent.putExtra(Intent.EXTRA_TITLE, "filename.txt");
//startActivityForResult 启动这个 Intent,等待用户选择位置,然后在回调中获取这个位置并写入数据
startActivityForResult(intent, CREATE_FILE_REQUEST_CODE);
} else {
// For Android 9 and below,Android 9 (API 28) 及以下版本:
//getExternalFilesDir(null)的值:/storage/emulated/0/Android/data/com.xxx.app/files
//yourfile.txt文件名写死了,只用于测试,为了用户体验需要使用自定义对话框或其他方法来处理文件创建和保存
File file = new File(getExternalFilesDir(null), "yourfile.txt");
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write("Your data".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
2.3.2 处理文件创建
重写 onActivityResult
来处理文件创建请求:
private static final int CREATE_FILE_REQUEST_CODE = 1;
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
// Assume we have a file type based on the file name
String fileName = getFileNameFromUri(uri);
writeTxtFile(uri);
}
}
}
}
2.3.3 写入文件内容
根据文件类型来生成相应的文件格式:
//writeTxtFile使用 PrintWriter 生成 txt 文件,并将其写入用户选择的文件 URI
private void writeTxtFile(Uri uri) {
try(OutputStream outputStream = getContentResolver().openOutputStream(uri);
PrintWriter writer = new PrintWriter(outputStream)) {
printWriter.println("Hello, World!");
printWriter.println("This is a test.");
} catch (IOException e) {
Log.d("test","File not found.");
e.printStackTrace();
}
}