ContentProvider再探——Document Provider

感谢你继续深入探讨ContentProvider中的DocumentsProvider!你提到“ContentProvider再探——Document Provider中文讲解”,结合之前的讨论,我推测你希望以中文深入了解DocumentsProvider的更多细节,可能需要更清晰的解释、进阶功能(如文件创建、目录支持、权限管理),或者与Android其他组件(如ServiceBroadcastReceiver)的结合,同时希望内容更贴合中文语境,易于理解。基于之前的TextDocumentsProvider示例,我将以通俗易懂的中文讲解,深入剖析DocumentsProvider的原理、实现和优化,扩展示例以支持更多功能(如文件创建、删除、目录导航),并整合ServiceBroadcastReceiver的协作。如果你有具体需求(例如特定功能、代码细节或场景),请进一步说明,我可以提供更精准的解答。


一、DocumentsProvider再探:核心概念(中文讲解)

1. 什么是DocumentsProvider?

  • 定义DocumentsProvider是Android中ContentProvider的一个子类,专门为存储访问框架(Storage Access Framework, SAF)设计,用于管理文档数据(如文本文件、图片、视频),让其他应用通过系统文件选择器访问你的应用数据。
  • 通俗解释:你可以把DocumentsProvider想象成一个“文件管理员”,它帮你的应用把文件或文件夹以标准化的方式展示给其他应用(比如文件管理器或微信),用户可以通过系统的文件选择界面挑文件,而不用直接访问你的应用数据库或存储。
  • 使用场景
  • 云存储:像Google Drive这样的应用,通过DocumentsProvider让用户访问云端文件。
  • 本地文件:你的应用可以暴露内部存储的文本、图片等文件。
  • 跨应用共享:让其他应用读取你的数据,比如一个笔记应用分享Markdown文件。

2. 与ContentProvider的区别

  • ContentProvider:通用数据提供者,支持任何结构化数据(数据库、文件等),通过ContentResolver提供queryinsert等操作。
  • DocumentsProvider:专为文件管理设计,基于SAF,处理文件和目录的元数据(如文件名、大小、MIME类型),支持系统文件选择器,API更专注于文件操作(如openDocumentcreateDocument)。
  • 关键点DocumentsProvider强制实现文件系统结构(根目录、子目录、文件),URI格式遵循DocumentsContract规范。

3. 核心方法(用中文解释)

以下是DocumentsProvider中必须实现或常用的方法:

  • queryRoots():定义文件系统的“根目录”,比如“内部存储”或“我的文档”。相当于告诉系统你的文件库从哪里开始。
  • queryChildDocuments():列出某个目录下的子文件或子目录,比如列出“我的文档”文件夹里的所有文件。
  • queryDocument():返回某个文件的详细信息(元数据),如文件名、大小、修改时间。
  • openDocument():打开文件,提供输入/输出流,让其他应用读取或写入内容。
  • createDocument():创建新文件或目录,响应用户在文件选择器中点击“新建”。
  • deleteDocument():删除文件或目录,支持用户删除操作。

4. 工作机制

  • SAF如何工作:用户通过Intent.ACTION_OPEN_DOCUMENTACTION_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:运行与测试
  1. 部署应用,启动MainActivity
  2. 检查系统文件选择器是否显示“文本文件”根目录,列出.txt文件和子目录。
  3. 测试创建新文件(ACTION_CREATE_DOCUMENT),验证文件出现在列表中。
  4. 测试删除文件,确认文件被移除。
  5. 模拟SD卡挂载(或通过ADB触发广播),检查StorageReceiver是否刷新列表。
  6. 查看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_MOUNTEDACTION_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. 性能优化

  • 异步查询:对大型目录,使用AsyncTaskExecutors处理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,请告诉我:

  1. 你想聚焦的具体功能(文件创建、目录树、云同步、缩略图)?
  2. 当前技术水平(熟悉ContentProvider、刚接触SAF、了解Android组件)?
  3. 是否需要更详细的代码(如openDocumentThumbnail、树形结构、云API集成)?
  4. 是否结合其他技术(如Room数据库、WorkManager、MVVM架构)?
  5. 是否需要特定场景的示例(比如支持PDF文件、外部存储)?

你可以基于上述示例扩展功能(如添加缩略图支持或云同步),或提出具体问题,我会提供更深入的代码或分析!

文章已创建 2349

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部