Android Q (10) 使用 DownloadManager 和 FileProvider 下载后打开 PDF

Open PDF after download with DownloadManager and FileProvider on Android Q (10)

targetSdkVersion: 30

在我们的应用程序中,我们有一个功能,我们将文件(主要是 pdf)下载到 public 下载文件夹,然后启动一个 intent 来打开它。我们的代码适用于 api >= 28 和 >= 30 的 android 应用程序。只有我们在 Android 10(sdkVersion 29)上的应用程序将尝试打开文档并立即关闭 activity 试图显示 pdf。 logcat 显示以下错误:

22-03-17 14:23:42.486 12161-15168/? E/DisplayData: openFd: java.io.FileNotFoundException: open failed: EACCES (Permission denied)
22-03-17 14:23:42.486 12161-15168/? E/PdfLoader: Can't load file (doesn't open)  Display Data [PDF : download.pdf] +ContentOpenable, uri: content://com.example.fileprovider/Download/download.pdf

如果我在应用程序设置中授予文件权限,它将完美运行,但如果我正确理解 android 文档,这应该没有必要,因为 Android 10。特别是因为没有Android 11 和 Android 12 台设备上没有此权限的问题。在所有 Android 版本中,文件将被正确下载,用户可以从其设备的下载部分手动打开它。

这是文件提供者的Android清单部分

<provider
    android:name="androidx.core.content.FileProvider"        
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
       android:name="android.support.FILE_PROVIDER_PATHS"
       android:resource="@xml/filepaths" />
</provider>

文件路径 XML 文件

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path
        name="external_files"
        path="." />
    <external-path
        name="Download"
        path="Download/"/>
    <files-path
        name="files"
        path="." />
    <external-cache-path
        name="external_cache"
        path="." />
</paths>

这是使用 DownloadManager 下载文件的代码

public static long downloadFile(Context context, String url, String fileName) {
    DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

    try {
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        request.setTitle(fileName)
                .setDescription(Localization.getStringClient("file_download_progress"))
                .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);

        return downloadManager.enqueue(request);
    } catch (Exception e) {
        e.printStackTrace();
        Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
    }
    return -1;
}

监听下载进度的广播接收器

public class DownloadBroadcastReceiver extends BroadcastReceiver {

    private long downloadId = -2;

    public void setDownloadId(long downloadId) {
        this.downloadId = downloadId;
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (id == downloadId) {
            DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(id);
            Cursor c = downloadManager.query(query);
            if (c != null) {
                c.moveToFirst();
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
                if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
                    String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                    String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
                    Uri fileUri = Uri.parse(uriString);

                    DownloadUtils.openFile(context, fileUri, mediaType);
                } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
                    Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
                }
                c.close();
            }
        }
    }
}

打开文件的代码。仅在 Android 10 时,应用程序再次立即关闭 activity。

public static void openFile(Context context, Uri fileUri, String mediaType) {
    File file = new File(fileUri.getPath());
    Uri uri = FileProvider.getUriForFile(
            context,
            context.getApplicationContext().getPackageName() + ".fileprovider",
            file
    );

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(uri, mediaType);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    try {
        context.startActivity(intent);
    } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
    }
}

下载调用看起来像这样

@Override
public void startPDFDownload(String pdfDownloadUrl, String fileName) {
    long downloadId = DownloadUtils.downloadFile(requireContext(), pdfDownloadUrl, fileName);

    if (downloadId > -1) {
        DownloadBroadcastReceiver receiver = new DownloadBroadcastReceiver();
        receiver.setDownloadId(downloadId);
        requireContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    }
}

我想我对 Android10 中文件处理的工作方式有错误的理解,但我不知道我必须在哪里调整代码或配置。非常感谢您的帮助。目前,作为一种解决方法,我们要求 WRITE_EXTERNAL_STORAGE 获得在 Android 10 上打开下载文件的权限。但我更愿意以正确的方式进行操作。

解法:

我将BroadcastReceiver调整为如下代码。我从光标中删除了 LOCAL_URI 并使用了 DownloadManager 方法中的 URI。

public class DownloadBroadcastReceiver extends BroadcastReceiver {

    private long downloadId = -2;

    public void setDownloadId(long downloadId) {
        this.downloadId = downloadId;
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (id == downloadId) {
            DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(id);
            Cursor c = downloadManager.query(query);
            if (c != null) {
                c.moveToFirst();
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
                if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
                    String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));

                    DownloadUtils.openFile(context, downloadManager.getUriForDownloadedFile(id), mediaType);
                } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
                    Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
                }
                c.close();
            }
        }
    }
}

并且我将使用uri打开文件的方法调整为以下代码。我删除了 FileProvider 代码并使用了 DownloadManager 中的 uri。

public static void openFile(Context context, Uri fileUri, String mediaType) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(fileUri, mediaType);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    try {
        context.startActivity(intent);
    } catch (ActivityNotFoundException e) {
        e.printStackTrace();
        Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
    }
}

其他方法代码不变

您不应使用 FileProvider 获取文件的 uri。

您可以从 DownloadManager 获取 uri 并使用它来提供您的文件。

所有 Android 版本的代码都相同。

不需要任何权限。