使用 FileProvider 打开 Android N 上下载的文件

Open downloaded file on Android N using FileProvider

由于 FileProvider 的更改,我必须为 Android N 修复我们的应用程序。我基本上已经阅读了最后一个主题的所有内容,但没有找到适合我的解决方案。

这是我们之前的代码,它开始从我们的应用程序下载,将它们存储在 Download 文件夹中,并在 DownloadManager 告诉他完成下载后立即调用 ACTION_VIEW 意图:

BroadcastReceiver onComplete = new BroadcastReceiver() {
    public void onReceive(Context ctxt, Intent intent) {
        Log.d(TAG, "Download commplete");

        // Check for our download
        long referenceId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
        if (mDownloadReference == referenceId) {
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(mDownloadReference);
            Cursor c = mDownloadManager.query(query);
            if (c.moveToFirst()) {
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
                if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
                    String localUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                    String fileExtension = MimeTypeMap.getFileExtensionFromUrl(localUri);
                    String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);

                    if (mimeType != null) {
                        Intent openFileIntent = new Intent(Intent.ACTION_VIEW);
                        openFileIntent.setDataAndTypeAndNormalize(Uri.parse(localUri), mimeType);
                        openFileIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);

                        try {
                            mAcme.startActivity(openFileIntent);
                        }
                        catch (ActivityNotFoundException e) {
                            // Ignore if no activity was found.
                        }
                    }
                }
            }
        }
    }
};

这适用于 Android M,但由于流行 FileUriExposedException 而无法用于 N。我现在尝试使用 FileProvider 来解决这个问题,但我无法让它工作。当我尝试获取内容 URI 时出现问题:

Failed to find configured root that contains /file:/storage/emulated/0/Download/test.pdf

从文件 DownloadManager 返回的 localUri 是:

file:///storage/emulated/0/Download/test.pdf

Environment.getExternalStorageDirectory() returns /storage/emulated/0 这是转换代码:

File file = new File(localUri);
Log.d(TAG, localUri + " - " + Environment.getExternalStorageDirectory());
Uri contentUri = FileProvider.getUriForFile(ctxt, "my.file.provider", file);

来自AndroidManifest.xml

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="my.file.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"/>
    </provider>

file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_path" path="." />
</paths>

而且我已经尝试了我能在 xml 文件中找到的所有值。 :(

此处更改 AndroidManifest.xml

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

更改文件路径

Uri contentUri;
if(Build.VERSION.SDK_INT == 24){
         contentUri = FileProvider.getUriForFile(MainActivity.this,
              getApplicationContext().getPackageName() + ".provider",
                    file);
 } else{
         contentUri = Uri.fromFile(file);
        }

多亏了@greenaps,我才得以解决这个问题。从 DownlodManager 检索到的本地 URI 以 file:// 为前缀。对于新的 FileProvider:

必须删除
if (localUri.substring(0, 7).matches("file://")) {
    localUri =  localUri.substring(7);
}
File file = new File(localUri);

我遇到了类似的问题,解决方法是不自动打开文件而是显示 'Download complete' 通知,然后让 Android 系统在用户单击通知时打开文件。

为了另外通知用户,我在下载完成时显示 Toast。

DownloadManager mManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

String url = "your URL";
String filename = "file.pdf";

// Set up the request.
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
            .setTitle("Test")
            .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
            .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
            .setDescription("Downloading...")
            .setMimeType("application/pdf");

request.allowScanningByMediaScanner();
mManager.enqueue(request);

广播接收器:

public class DownloadReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        switch (intent.getAction()) {
            case DownloadManager.ACTION_DOWNLOAD_COMPLETE:
                Toast.makeText(context, "Download completed", Toast.LENGTH_SHORT).show();
                break;
        }
    }

}

可以有更清晰的解决方案吗?

Uri uriParser = Uri.parse(downloadedPackageUriString);
File downloadedFile = new File(uriParser.getPath());

之前

file:///storage/emulated/0/Download/ZS%20Gac%C3%ADkov%C3%A1.pdf

之后

/storage/emulated/0/Download/ZS Gacíková.pdf

如何获取文件的确切路径文件名。越来越低的设备。

public class RealPathUtil {

public static String getRealPath(Context context, Uri fileUri) {
    String realPath;
    // SDK < API11
    if (Build.VERSION.SDK_INT < 11) {
        realPath = RealPathUtil.getRealPathFromURI_BelowAPI11(context, fileUri);
    }
    // SDK >= 11 && SDK < 19
    else if (Build.VERSION.SDK_INT < 19) {
        realPath = RealPathUtil.getRealPathFromURI_API11to18(context, fileUri);
    }
    // SDK > 19 (Android 4.4) and up
    else {
        realPath = RealPathUtil.getRealPathFromURI_API19(context, fileUri);
    }
    return realPath;
}


@SuppressLint("NewApi")
public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) {
    String[] proj = {MediaStore.Images.Media.DATA};
    String result = null;

    CursorLoader cursorLoader = new CursorLoader(context, contentUri, proj, null, null, null);
    Cursor cursor = cursorLoader.loadInBackground();

    if (cursor != null) {
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        result = cursor.getString(column_index);
        cursor.close();
    }
    return result;
}

public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) {
    String[] proj = {MediaStore.Images.Media.DATA};
    Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
    int column_index = 0;
    String result = "";
    if (cursor != null) {
        column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        result = cursor.getString(column_index);
        cursor.close();
        return result;
    }
    return result;
}

/**
 * Get a file path from a Uri. This will get the the path for Storage Access
 * Framework Documents, as well as the _data field for the MediaStore and
 * other file-based ContentProviders.
 *
 * @param context The context.
 * @param uri     The Uri to query.
 * @author paulburke
 */
@SuppressLint("NewApi")
public static String getRealPathFromURI_API19(final Context context, final Uri uri) {

    final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;

    // DocumentProvider
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
        // ExternalStorageProvider
        if (isExternalStorageDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];

            if ("primary".equalsIgnoreCase(type)) {
                return Environment.getExternalStorageDirectory() + "/" + split[1];
            }

            // TODO handle non-primary volumes
        }
        // DownloadsProvider
        else if (isDownloadsDocument(uri)) {

            final String id = DocumentsContract.getDocumentId(uri);
            final Uri contentUri = ContentUris.withAppendedId(
                    Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

            return getDataColumn(context, contentUri, null, null);
        }
        // MediaProvider
        else if (isMediaDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];

            Uri contentUri = null;
            if ("image".equals(type)) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            } else if ("video".equals(type)) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            } else if ("audio".equals(type)) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            }

            final String selection = "_id=?";
            final String[] selectionArgs = new String[]{
                    split[1]
            };

            return getDataColumn(context, contentUri, selection, selectionArgs);
        }
    }
    // MediaStore (and general)
    else if ("content".equalsIgnoreCase(uri.getScheme())) {

        // Return the remote address
        if (isGooglePhotosUri(uri))
            return uri.getLastPathSegment();

        return getDataColumn(context, uri, null, null);
    }
    // File
    else if ("file".equalsIgnoreCase(uri.getScheme())) {
        return uri.getPath();
    }

    return null;
}

/**
 * Get the value of the data column for this Uri. This is useful for
 * MediaStore Uris, and other file-based ContentProviders.
 *
 * @param context       The context.
 * @param uri           The Uri to query.
 * @param selection     (Optional) Filter used in the query.
 * @param selectionArgs (Optional) Selection arguments used in the query.
 * @return The value of the _data column, which is typically a file path.
 */
public static String getDataColumn(Context context, Uri uri, String selection,
                                   String[] selectionArgs) {

    Cursor cursor = null;
    final String column = "_data";
    final String[] projection = {
            column
    };

    try {
        cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                null);
        if (cursor != null && cursor.moveToFirst()) {
            final int index = cursor.getColumnIndexOrThrow(column);
            return cursor.getString(index);
        }
    } finally {
        if (cursor != null)
            cursor.close();
    }
    return null;
}


/**
 * @param uri The Uri to check.
 * @return Whether the Uri authority is ExternalStorageProvider.
 */
public static boolean isExternalStorageDocument(Uri uri) {
    return "com.android.externalstorage.documents".equals(uri.getAuthority());
}

/**
 * @param uri The Uri to check.
 * @return Whether the Uri authority is DownloadsProvider.
 */
public static boolean isDownloadsDocument(Uri uri) {
    return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}

/**
 * @param uri The Uri to check.
 * @return Whether the Uri authority is MediaProvider.
 */
public static boolean isMediaDocument(Uri uri) {
    return "com.android.providers.media.documents".equals(uri.getAuthority());
}

/**
 * @param uri The Uri to check.
 * @return Whether the Uri authority is Google Photos.
 */
public static boolean isGooglePhotosUri(Uri uri) {
    return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}

}

这是精确解。只需打电话 Uri selectedResume = data.getData(); String mediaResumePath = RealPathUtil.getRealPath(this, selectedResume);

这就是我下载、保存并return一个文件到视图模型的方式:)

接口文件下载器{ 伴生对象{ 私有 val 实例:FileDownloader by lazy {

        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS

        val client = OkHttpClient.Builder()
            .connectTimeout(120, TimeUnit.SECONDS)
            .readTimeout(300, TimeUnit.SECONDS)
            .callTimeout(300, TimeUnit.SECONDS)
            .addInterceptor(loggingInterceptor)
            .build()

        val downloaderApi = Retrofit.Builder()
            .baseUrl("https://exmaple.com")
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
            .create(FileDownloader::class.java)

        downloaderApi
    }

    suspend fun downloadAndSaveFile(
        url: String
    ): File {
        val file = getPdfFile()
        val responseBody = try {
            instance.downloadFile(url)
        } catch (e: Exception) {
            throw FileDownloadException(url, e)
        }
        @Suppress("BlockingMethodInNonBlockingContext")
        try {
            FileOutputStream(file).use { outputStream ->
                responseBody.byteStream().use { inputStream ->
                    inputStream.copyTo(outputStream)
                }
            }
        } catch (e: Exception) {
            throw FileDownloadException(url, e)
        }
        return file
    }


}

@GET
@Streaming
suspend fun downloadFile(@Url utl: String): ResponseBody
}

fun getPdfFile(): File {
    File(App.appContext.filesDir, "pdfs").apply { mkdirs() }
    return File(App.appContext.filesDir, "tempPDF.pdf")
}

class FileDownloadException(url: String, e: Exception) : Throwable()