You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android 7.0 Nougat 不引入Support库如何实现FileProvider功能

零依赖实现Android 7.0+ 等效FileProvider文件共享方案

Support库提供的FileProvider本质是对系统原生ContentProvider的轻量封装,没有特殊私有API依赖,完全可以自行实现同等逻辑,无需引入Support v4或AndroidX组件,配合系统版本分支判断即可覆盖所有场景。

版本分支规则

  • Android 6.0(API 23)及以下:直接使用原有file://格式Uri传递文件,无需额外处理
  • Android 7.0(API 24)及以上:使用自定义原生ContentProvider生成content://格式Uri,为接收方授予临时文件访问权限,完全规避FileUriExposedException

具体实现

1. 实现原生文件提供者

直接继承系统框架层的android.content.ContentProvider,实现文件访问、查询的核心逻辑:

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class NativeFileProvider extends ContentProvider {
    private static final String[] DEFAULT_COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
    private File sharedRootDir;

    @Override
    public boolean onCreate() {
        // 可根据业务需求替换为需要共享的根目录,如外部缓存目录getExternalCacheDir()
        sharedRootDir = getContext().getFilesDir();
        return true;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        File targetFile = getFileFromUri(uri);
        int accessMode = ParcelFileDescriptor.MODE_READ_ONLY;
        if (mode.contains("w")) accessMode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
        return ParcelFileDescriptor.open(targetFile, accessMode);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        File targetFile = getFileFromUri(uri);
        if (projection == null) projection = DEFAULT_COLUMNS;
        MatrixCursor cursor = new MatrixCursor(projection, 1);
        Object[] rowValues = new Object[projection.length];
        for (int i = 0; i < projection.length; i++) {
            String column = projection[i];
            if (OpenableColumns.DISPLAY_NAME.equals(column)) {
                rowValues[i] = targetFile.getName();
            } else if (OpenableColumns.SIZE.equals(column)) {
                rowValues[i] = targetFile.length();
            }
        }
        cursor.addRow(rowValues);
        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        File targetFile = getFileFromUri(uri);
        return getContext().getContentResolver().getType(Uri.fromFile(targetFile));
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("Operation not supported");
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Operation not supported");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Operation not supported");
    }

    public static Uri getUriFromFile(Context context, File file) {
        String targetPath;
        try {
            targetPath = file.getCanonicalPath();
        } catch (IOException e) {
            throw new IllegalArgumentException("Invalid file path");
        }
        File rootPath;
        try {
            rootPath = context.getFilesDir().getCanonicalFile();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (!targetPath.startsWith(rootPath.getPath())) {
            throw new IllegalArgumentException("File is outside shared directory");
        }
        String relativePath = targetPath.substring(rootPath.getPath().length() + 1);
        return new Uri.Builder()
                .scheme(ContentResolver.SCHEME_CONTENT)
                .authority(context.getPackageName() + ".nativefileprovider")
                .encodedPath(Uri.encode(relativePath, "/"))
                .build();
    }

    private File getFileFromUri(Uri uri) {
        String encodedPath = uri.getEncodedPath();
        if (encodedPath == null) throw new IllegalArgumentException("Invalid uri");
        String relativePath = Uri.decode(encodedPath.substring(1));
        File targetFile = new File(sharedRootDir, relativePath);
        try {
            if (!targetFile.getCanonicalPath().startsWith(sharedRootDir.getCanonicalPath())) {
                throw new SecurityException("Illegal path access");
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("Invalid file");
        }
        return targetFile;
    }
}

2. 清单文件注册组件

在AndroidManifest.xml中注册自定义Provider,关闭公开访问权限、开启临时Uri授权:

<provider
    android:name=".NativeFileProvider"
    android:authorities="${applicationId}.nativefileprovider"
    android:exported="false"
    android:grantUriPermissions="true" />

3. 跨应用文件传递逻辑

在需要向外部应用传递文件的位置,按系统版本走分支逻辑:

public void dispatchFileIntent(Context context, Intent targetIntent, File shareFile) {
    Uri fileUri;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        fileUri = NativeFileProvider.getUriFromFile(context, shareFile);
        // 授予接收方临时读权限,需要写权限则追加FLAG_GRANT_WRITE_URI_PERMISSION
        targetIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
        fileUri = Uri.fromFile(shareFile);
    }
    // 根据业务场景替换Extra字段,如拍照场景用MediaStore.EXTRA_OUTPUT、分享场景用Intent.EXTRA_STREAM
    targetIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
    context.startActivity(targetIntent);
}

关键注意事项

  • 必须保留路径合法性校验逻辑,拦截../这类路径遍历请求,避免应用私有目录被恶意越权访问
  • 若需要共享多个目录(外部缓存、公共存储目录等),可在Provider内增加路径映射表,逻辑与Support库FileProvider的xml路径配置规则完全一致
  • 临时访问权限会跟随接收方组件生命周期自动回收,无需手动释放;如果使用选择器分享文件,建议提前遍历所有匹配Intent的应用包名,主动调用grantUriPermission授权,避免部分定制系统上Intent flag授权失效
  • 该实现所有API均为系统原生公开API,不存在版本兼容问题,在Android 7.0及以上系统的行为与官方Support库FileProvider完全一致

内容的提问来源于stack exchange,提问作者htfree

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 07:06:19