感谢你继续深入探讨ContentProvider中的DocumentsProvider!你提到“ContentProvider再探——Document Provider中文讲解”,结合之前的讨论,我推测你希望以中文深入了解DocumentsProvider的更多细节,可能需要更清晰的解释、进阶功能(如文件创建、目录支持、权限管理),或者与Android其他组件(如Service、BroadcastReceiver)的结合,同时希望内容更贴合中文语境,易于理解。基于之前的TextDocumentsProvider示例,我将以通俗易懂的中文讲解,深入剖析DocumentsProvider的原理、实现和优化,扩展示例以支持更多功能(如文件创建、删除、目录导航),并整合Service和BroadcastReceiver的协作。如果你有具体需求(例如特定功能、代码细节或场景),请进一步说明,我可以提供更精准的解答。
一、DocumentsProvider再探:核心概念(中文讲解)
1. 什么是DocumentsProvider?
- 定义:
DocumentsProvider是Android中ContentProvider的一个子类,专门为存储访问框架(Storage Access Framework, SAF)设计,用于管理文档数据(如文本文件、图片、视频),让其他应用通过系统文件选择器访问你的应用数据。 - 通俗解释:你可以把
DocumentsProvider想象成一个“文件管理员”,它帮你的应用把文件或文件夹以标准化的方式展示给其他应用(比如文件管理器或微信),用户可以通过系统的文件选择界面挑文件,而不用直接访问你的应用数据库或存储。 - 使用场景:
- 云存储:像Google Drive这样的应用,通过
DocumentsProvider让用户访问云端文件。 - 本地文件:你的应用可以暴露内部存储的文本、图片等文件。
- 跨应用共享:让其他应用读取你的数据,比如一个笔记应用分享Markdown文件。
2. 与ContentProvider的区别
- ContentProvider:通用数据提供者,支持任何结构化数据(数据库、文件等),通过
ContentResolver提供query、insert等操作。 - DocumentsProvider:专为文件管理设计,基于SAF,处理文件和目录的元数据(如文件名、大小、MIME类型),支持系统文件选择器,API更专注于文件操作(如
openDocument、createDocument)。 - 关键点:
DocumentsProvider强制实现文件系统结构(根目录、子目录、文件),URI格式遵循DocumentsContract规范。
3. 核心方法(用中文解释)
以下是DocumentsProvider中必须实现或常用的方法:
queryRoots():定义文件系统的“根目录”,比如“内部存储”或“我的文档”。相当于告诉系统你的文件库从哪里开始。queryChildDocuments():列出某个目录下的子文件或子目录,比如列出“我的文档”文件夹里的所有文件。queryDocument():返回某个文件的详细信息(元数据),如文件名、大小、修改时间。openDocument():打开文件,提供输入/输出流,让其他应用读取或写入内容。createDocument():创建新文件或目录,响应用户在文件选择器中点击“新建”。deleteDocument():删除文件或目录,支持用户删除操作。
4. 工作机制
- SAF如何工作:用户通过
Intent.ACTION_OPEN_DOCUMENT或ACTION_OPEN_DOCUMENT_TREE启动系统文件选择器,系统调用你的DocumentsProvider来获取文件列表或打开文件。 - URI格式:使用
content://开头的URI,比如content://com.example.textdocuments/document/123。 - 权限管理:SAF通过临时URI权限(
FLAG_GRANT_READ_URI_PERMISSION)确保安全访问。
二、DocumentsProvider实践:进阶文本文件管理
基于之前的TextDocumentsProvider示例,我们扩展功能,打造一个更完整的文档提供者,支持:
- 文件创建:允许用户通过SAF创建新文本文件。
- 目录支持:支持文件夹导航,允许创建子目录。
- Service协作:通过
Service异步创建文件并记录日志。 - BroadcastReceiver集成:监听存储变化(如SD卡插入),刷新文件列表。
1. 项目目标
- 功能:提供一个文件系统,包含根目录“Text Files”和子目录,用户可通过系统文件选择器浏览、创建、删除文本文件或文件夹。
- 场景:
Service定时创建新文本文件并记录日志。BroadcastReceiver监听外部存储事件,通知DocumentsProvider刷新。- 用户通过SAF选择文件或创建新文件。
2. 实现步骤
步骤1:增强DocumentsProvider
扩展TextDocumentsProvider,支持文件创建、删除和目录导航。
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class TextDocumentsProvider extends DocumentsProvider {
private static final String AUTHORITY = "com.example.textdocuments";
private static final String ROOT_ID = "root";
private static final String[] DEFAULT_ROOT_PROJECTION = {
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE,
Document.COLUMN_FLAGS,
Document.COLUMN_LAST_MODIFIED
};
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryRoots(String[] projection) {
MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
cursor.newRow()
.add(Root.COLUMN_ROOT_ID, ROOT_ID)
.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD)
.add(Root.COLUMN_TITLE, "文本文件")
.add(Root.COLUMN_DOCUMENT_ID, ROOT_ID);
return cursor;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) {
MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
File parent = new File(parentDocumentId.equals(ROOT_ID) ? getContext().getFilesDir() + "/text_files" : parentDocumentId);
if (!parent.exists()) parent.mkdirs();
for (File file : parent.listFiles()) {
cursor.newRow()
.add(Document.COLUMN_DOCUMENT_ID, file.getAbsolutePath())
.add(Document.COLUMN_DISPLAY_NAME, file.getName())
.add(Document.COLUMN_MIME_TYPE, file.isDirectory() ? Document.MIME_TYPE_DIR : "text/plain")
.add(Document.COLUMN_SIZE, file.length())
.add(Document.COLUMN_FLAGS,
(file.isDirectory() ? Document.FLAG_DIR_SUPPORTS_CREATE : 0) |
Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE)
.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
}
return cursor;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) {
MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
File file = new File(documentId);
if (file.exists()) {
cursor.newRow()
.add(Document.COLUMN_DOCUMENT_ID, documentId)
.add(Document.COLUMN_DISPLAY_NAME, file.getName())
.add(Document.COLUMN_MIME_TYPE, file.isDirectory() ? Document.MIME_TYPE_DIR : "text/plain")
.add(Document.COLUMN_SIZE, file.length())
.add(Document.COLUMN_FLAGS,
(file.isDirectory() ? Document.FLAG_DIR_SUPPORTS_CREATE : 0) |
Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE)
.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
}
return cursor;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
File parent = new File(parentDocumentId.equals(ROOT_ID) ? getContext().getFilesDir() + "/text_files" : parentDocumentId);
File newFile = new File(parent, displayName);
try {
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
newFile.mkdir();
} else {
newFile.createNewFile();
FileOutputStream fos = new FileOutputStream(newFile);
fos.write("新文件已创建".getBytes());
fos.close();
}
getContext().getContentResolver().notifyChange(
android.provider.DocumentsContract.buildDocumentUri(AUTHORITY, newFile.getAbsolutePath()), null);
return newFile.getAbsolutePath();
} catch (IOException e) {
throw new FileNotFoundException("无法创建文件: " + e.getMessage());
}
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = new File(documentId);
if (!file.exists()) {
throw new FileNotFoundException("文件不存在: " + documentId);
}
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
getContext().getContentResolver().notifyChange(
android.provider.DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null);
}
private void deleteDirectory(File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
deleteDirectory(file);
} else {
file.delete();
}
}
dir.delete();
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
File file = new File(documentId);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint, CancellationSignal signal) {
return null; // 暂不支持缩略图
}
}
代码说明:
- 新增功能:
createDocument():支持创建文件或目录,用户可在文件选择器中新建文本文件或文件夹。deleteDocument():支持删除文件或目录(递归删除目录内容)。queryChildDocuments():支持目录,区分文件和文件夹(Document.MIME_TYPE_DIR)。- 通知变化:每次创建或删除文件后,调用
notifyChange()刷新文件列表。
步骤2:增强Service
创建一个FileCreationService,定时创建新文件并记录日志。
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class FileCreationService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(() -> {
File dir = new File(getFilesDir(), "text_files");
if (!dir.exists()) dir.mkdirs();
File file = new File(dir, "笔记_" + System.currentTimeMillis() + ".txt");
try {
FileWriter writer = new FileWriter(file);
writer.write("新笔记创建于: " + System.currentTimeMillis());
writer.close();
Log.d("FileCreationService", "创建文件: " + file.getName());
// 通知ContentResolver数据变化
getContentResolver().notifyChange(
android.provider.DocumentsContract.buildDocumentUri("com.example.textdocuments", file.getAbsolutePath()), null);
} catch (IOException e) {
Log.e("FileCreationService", "创建文件失败", e);
}
}).start();
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
代码说明:
- Service异步创建文本文件,写入时间戳。
- 使用
notifyChange()通知DocumentsProvider刷新文件列表。
步骤3:添加BroadcastReceiver
创建一个StorageReceiver,监听外部存储事件(如SD卡插入),刷新文件列表。
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.provider.DocumentsContract;
import android.util.Log;
public class StorageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction())) {
Log.d("StorageReceiver", "检测到存储设备挂载");
// 通知DocumentsProvider刷新
context.getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri("com.example.textdocuments", "root"), null);
}
}
}
代码说明:
- 监听
ACTION_MEDIA_MOUNTED,触发DocumentsProvider刷新根目录。 - 需动态或静态注册(见步骤4)。
步骤4:注册组件
在AndroidManifest.xml中声明所有组件:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<provider
android:name=".TextDocumentsProvider"
android:authorities="com.example.textdocuments"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<service android:name=".FileCreationService" />
<receiver android:name=".StorageReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<data android:scheme="file" />
</intent-filter>
</receiver>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
说明:
provider设置exported="true",允许SAF访问。receiver静态注册,监听存储事件。- 添加存储权限以支持外部存储操作。
步骤5:Activity测试
在MainActivity中启动文件选择器,测试功能。
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_OPEN_DOCUMENT = 1;
private static final int REQUEST_CREATE_DOCUMENT = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 启动Service创建文件
startService(new Intent(this, FileCreationService.class));
// 打开文件选择器
Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openIntent.addCategory(Intent.CATEGORY_OPENABLE);
openIntent.setType("text/plain");
startActivityForResult(openIntent, REQUEST_OPEN_DOCUMENT);
// 测试创建文件
Intent createIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
createIntent.addCategory(Intent.CATEGORY_OPENABLE);
createIntent.setType("text/plain");
createIntent.putExtra(Intent.EXTRA_TITLE, "新笔记.txt");
startActivityForResult(createIntent, REQUEST_CREATE_DOCUMENT);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && data != null) {
if (requestCode == REQUEST_OPEN_DOCUMENT) {
Log.d("MainActivity", "选择文件: " + data.getData());
} else if (requestCode == REQUEST_CREATE_DOCUMENT) {
Log.d("MainActivity", "创建文件: " + data.getData());
}
}
}
}
代码说明:
- 测试
ACTION_OPEN_DOCUMENT(打开文件)和ACTION_CREATE_DOCUMENT(创建文件)。 - 启动
FileCreationService生成测试文件。
步骤6:运行与测试
- 部署应用,启动
MainActivity。 - 检查系统文件选择器是否显示“文本文件”根目录,列出
.txt文件和子目录。 - 测试创建新文件(
ACTION_CREATE_DOCUMENT),验证文件出现在列表中。 - 测试删除文件,确认文件被移除。
- 模拟SD卡挂载(或通过ADB触发广播),检查
StorageReceiver是否刷新列表。 - 查看Logcat日志,确认Service和Receiver的行为。
三、DocumentsProvider进阶:优化与扩展
1. 与Service和BroadcastReceiver的深度协作
- Service:
- 用途:异步管理文件(如批量生成、上传到云端)。
- 优化:使用
ForegroundService(Android 8.0+)避免后台限制:java @Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("文件服务") .setContentText("正在创建文件") .setSmallIcon(android.R.drawable.ic_dialog_info) .build(); startForeground(1, notification); // 异步创建文件 return START_STICKY; } - BroadcastReceiver:
- 用途:监听系统事件(如
ACTION_MEDIA_MOUNTED、ACTION_MEDIA_REMOVED),触发DocumentsProvider刷新。 - 动态注册:在Activity中动态注册
StorageReceiver,更灵活:java StorageReceiver receiver = new StorageReceiver(); IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED); filter.addDataScheme("file"); registerReceiver(receiver, filter);
2. 性能优化
- 异步查询:对大型目录,使用
AsyncTask或Executors处理queryChildDocuments():
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) {
AsyncTask.execute(() -> {
// 异步加载文件列表
});
return new MatrixCursor(projection);
}
- 分页加载:支持
DocumentsContract.EXTRA_LOADING实现懒加载,减少首次加载时间。 - 缓存:缓存文件元数据到内存,减少IO操作。
3. 安全与权限
- 权限控制:
- 使用
android:permission="android.permission.MANAGE_DOCUMENTS"限制访问。 - 动态授予URI权限:
java getContext().grantUriPermission("com.example.client", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - 安全传输:确保
openDocument()返回安全的文件流,避免泄露敏感数据。
4. 扩展功能
- 支持树形结构:实现
ACTION_OPEN_DOCUMENT_TREE,允许用户选择整个目录:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_OPEN_DOCUMENT_TREE);
- 云同步:结合
SyncAdapter或云API(如Google Drive API)同步文件。 - 缩略图支持:实现
openDocumentThumbnail()为图片文件生成预览:
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) {
File file = new File(documentId);
return new AssetFileDescriptor(ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), 0, file.length());
}
四、学习与实践资源(中文)
- 官方文档:Android开发者 – DocumentsProvider
- 书籍:《Android编程权威指南》(中文版,Big Nerd Ranch出品)
- 课程:B站或慕课网的Android开发进阶课程,搜索“Storage Access Framework”或“DocumentsProvider”。
- 实践:
- 扩展示例,支持图片文件预览(
openDocumentThumbnail)。 - 实现一个云存储
DocumentsProvider,连接阿里云或百度云。
五、下一步
为了更精准地“再探”DocumentsProvider,请告诉我:
- 你想聚焦的具体功能(文件创建、目录树、云同步、缩略图)?
- 当前技术水平(熟悉
ContentProvider、刚接触SAF、了解Android组件)? - 是否需要更详细的代码(如
openDocumentThumbnail、树形结构、云API集成)? - 是否结合其他技术(如Room数据库、WorkManager、MVVM架构)?
- 是否需要特定场景的示例(比如支持PDF文件、外部存储)?
你可以基于上述示例扩展功能(如添加缩略图支持或云同步),或提出具体问题,我会提供更深入的代码或分析!