Firebase Storage 下载文件到 Android Q 中的图片或电影文件夹

Firebase Storage download file to Pictures or Movie folder in Android Q

我能够从 Firebase Storage 下载一个文件到 storage/emulated/0/Pictures,这是一个默认的图片文件夹,大多数流行的应用程序都在使用它,例如 Facebook 或 Instagram。现在 Android Q 在存储和访问文件方面有很多行为变化,当 Android Q 中的 运行 时,我的应用程序不再能够从存储桶下载文件。

这是将文件从 Firebase 存储桶写入和下载到大容量存储默认文件夹(如图片、电影、文档等)的代码。它适用于 Android M,但不适用于 Q工作。

    String state = Environment.getExternalStorageState();

    String type = "";

    if (downloadUri.contains("jpg") || downloadUri.contains("jpeg")
        || downloadUri.contains("png") || downloadUri.contains("webp")
        || downloadUri.contains("tiff") || downloadUri.contains("tif")) {
        type = ".jpg";
        folderName = "Images";
    }
    if (downloadUri.contains(".gif")){
        type = ".gif";
        folderName = "Images";
    }
    if (downloadUri.contains(".mp4") || downloadUri.contains(".avi")){
        type = ".mp4";
        folderName = "Videos";
    }


    //Create a path from root folder of primary storage
    File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + Environment.DIRECTORY_PICTURES + "/MY_APP_NAME");
    if (Environment.MEDIA_MOUNTED.equals(state)){
        try {
            if (dir.mkdirs())
                Log.d(TAG, "New folder is created.");
        }
        catch (Exception e) {
            e.printStackTrace();
            Crashlytics.logException(e);
        }
    }

    //Create a new file
    File filePath = new File(dir, UUID.randomUUID().toString() + type);

    //Creating a reference to the link
    StorageReference httpsReference = FirebaseStorage.getInstance().getReferenceFromUrl(download_link_of_file_from_Firebase_Storage_bucket);

    //Getting the file from the server
    httpsReference.getFile(filePath).addOnProgressListener(taskSnapshot ->                                 

showProgressNotification(taskSnapshot.getBytesTransferred(), taskSnapshot.getTotalByteCount(), requestCode)

);

有了它,它将使用路径 storage/emulated/0/Pictures/MY_APP_NAME 从服务器将文件下载到您的设备存储,但是对于 Android Q 这将不再有效,因为许多 API 已被弃用,例如 Environment.getExternalStorageDirectory()

使用 android:requestLegacyExternalStorage=true 是一个临时解决方案,但很快将不再适用于 Android 11 及更高版本。

所以我的问题是如何在根目录中的默认 PictureMovie 文件夹中使用 Firebase Storage API 下载文件Android/data/com.myapp.package/files.

MediaStoreContentResolver 有解决办法吗?我需要应用哪些更改?

这是我的解决方案:

使用 Glide 下载文件

public void downloadFile(Context context, String url){
    String mimeType = getMimeType(url);
    Glide.with(context).asFile().load(url).listener(new RequestListener<File>() {
        @Override
        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
            return false;
        }

        @Override
        public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
            saveFile(context,resource, mimeType);
            return false;
        }
    }).submit();
}

获取文件 mimeType

 public static String getMimeType(String url) {
    String mimeType = null;
    String extension = MimeTypeMap.getFileExtensionFromUrl(url);
    if (extension != null) {
        mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }
    return mimeType;
}

并将文件保存到外部存储器

public  Uri saveFile(Context context, File file, String mimeType) {
    String folderName = "Pictures";
    String extension = ".jpg";
    if(mimeType.endsWith("gif")){
        extension = ".gif";
    }else if(mimeType.startsWith("image/")){
        extension = ".jpg";
    }else if(mimeType.startsWith("video/")){
        extension = ".mp4";
        folderName = "Movies";
    }else if(mimeType.startsWith("audio/")){
        extension = ".mp3";
        folderName = "Music";
    }else if(mimeType.endsWith("pdf")){
        extension = ".pdf";
        folderName = "Documents";
    }
    if (android.os.Build.VERSION.SDK_INT >= 29) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.MIME_TYPE, mimeType);
        values.put(MediaStore.Files.FileColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
        values.put(MediaStore.Files.FileColumns.DATE_TAKEN, System.currentTimeMillis());
        values.put(MediaStore.Files.FileColumns.RELATIVE_PATH, folderName);
        values.put(MediaStore.Files.FileColumns.IS_PENDING, true);
        values.put(MediaStore.Files.FileColumns.DISPLAY_NAME, "file_" + System.currentTimeMillis() + extension);
        Uri uri = null;
        if(mimeType.startsWith("image/")){
            uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.startsWith("video/")){
            uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.startsWith("audio/")){
            uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
        }else if(mimeType.endsWith("pdf")){
            uri = context.getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);
        }
        if (uri != null) {
            try {
                saveFileToStream(context.getContentResolver().openInputStream(Uri.fromFile(file)), context.getContentResolver().openOutputStream(uri));
                values.put(MediaStore.Video.Media.IS_PENDING, false);
                context.getContentResolver().update(uri, values, null, null);
                return uri;
            } catch (IOException e) {
                e.printStackTrace();
            }

            return null;
        }
    }
    return null;
}

将文件保存到流

private void saveFileToStream(InputStream input, OutputStream outputStream) throws IOException {
    try {
        try (OutputStream output = outputStream ){
            byte[] buffer = new byte[4 * 1024]; // or other buffer size
            int read;

            while ((read = input.read(buffer)) != -1) {
                output.write(buffer, 0, read);
            }
            output.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    } finally {
        input.close();
    }
}

我用模拟器试过Android 29。它工作正常。

注意getExternalStorageDirectory() 在 API 级别 29 中被弃用。为了提高用户隐私,直接访问 shared/external 存储设备已弃用。当应用面向 Build.VERSION_CODES.Q 时,应用无法再直接访问从此方法返回的路径。通过迁移到 Context#getExternalFilesDir(String)、MediaStore 或 Intent#ACTION_OPEN_DOCUMENT.

等替代方案,应用可以继续访问存储在 shared/external 存储中的内容

==新答案==

如果您想监控下载进度,可以像这样使用 getStream() FirebaseStorage SDK:

httpsReference.getStream((state, inputStream) -> {

                long totalBytes = state.getTotalByteCount();
                long bytesDownloaded = 0;

                byte[] buffer = new byte[1024];
                int size;

                while ((size = inputStream.read(buffer)) != -1) {
                    stream.write(buffer, 0, size);
                    bytesDownloaded += size;
                    showProgressNotification(bytesDownloaded, totalBytes, requestCode);
                }

                // Close the stream at the end of the Task
                inputStream.close();
                stream.flush();
                stream.close();

            }).addOnSuccessListener(taskSnapshot -> {
                showDownloadFinishedNotification(downloadedFileUri, downloadURL, true, requestCode);
                //Mark task as complete so the progress download notification whether success of fail will become removable
                taskCompleted();
                contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, false);
                resolver.update(uriResolve, contentValues, null, null);
            }).addOnFailureListener(e -> {
                Log.w(TAG, "download:FAILURE", e);

                try {
                    stream.flush();
                    stream.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                    FirebaseCrashlytics.getInstance().recordException(ioException);
                }

                FirebaseCrashlytics.getInstance().recordException(e);

                //Send failure
                showDownloadFinishedNotification(null, downloadURL, false, requestCode);

                //Mark task as complete
                taskCompleted();
            });

==旧答案==

经过数小时的努力,我终于成功了,但最后还是使用 .getBytes(maximum_file_size) 而不是 .getFile(file_object)

非常感谢@Kasim 提出 getBytes(maximum_file_size) 的想法以及使用 InputStreamOutputStream 的示例代码。通过搜索 S.O 相关主题对I/O也是一个很大的帮助 and here

这里的想法是 .getByte(maximum_file_size) 将从存储桶下载文件并 return 在其 addOnSuccessListener 回调中 byte[]。缺点是你必须指定你允许下载的文件大小并且没有下载进度计算可以完成 AFAIK 除非你用 outputStream.write(0,0,0); 做一些工作我试着像 here 一样逐块写它但是解决方案正在抛出 ArrayIndexOutOfBoundsException,因为您必须准确地处理数组中的索引。

下面是让您将文件从 Firebase 存储桶保存到设备默认目录的代码:storage/emulated/0/Pictures, storage/emulated/0/Movies, storage/emulated/0/Documents, you name it

//Member variable but depending on your scope
private ByteArrayInputStream inputStream;
private Uri downloadedFileUri;
private OutputStream stream;

//Creating a reference to the link
    StorageReference httpsReference = FirebaseStorage.getInstance().getReferenceFromUrl(downloadURL);

    Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    String type = "";
    String mime = "";
    String folderName = "";

    if (downloadURL.contains("jpg") || downloadURL.contains("jpeg")
            || downloadURL.contains("png") || downloadURL.contains("webp")
            || downloadURL.contains("tiff") || downloadURL.contains("tif")) {
        type = ".jpg";
        mime = "image/*";
        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_PICTURES;
    }
    if (downloadURL.contains(".gif")){
        type = ".gif";
        mime = "image/*";
        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_PICTURES;
    }
    if (downloadURL.contains(".mp4") || downloadURL.contains(".avi")){
        type = ".mp4";
        mime = "video/*";
        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_MOVIES;
    }
    if (downloadURL.contains(".mp3")){
        type = ".mp3";
        mime = "audio/*";
        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
        folderName = Environment.DIRECTORY_MUSIC;
    }

            final String relativeLocation = folderName + "/" + getString(R.string.app_name);

        final ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, UUID.randomUUID().toString() + type);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mime); //Cannot be */*
        contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation);

        ContentResolver resolver = getContentResolver();
        Uri uriResolve = resolver.insert(contentUri, contentValues);

        try {

            if (uriResolve == null || uriResolve.getPath() == null) {
                throw new IOException("Failed to create new MediaStore record.");
            }

            stream = resolver.openOutputStream(uriResolve);

            //This is 1GB change this depending on you requirements
            httpsReference.getBytes(1024 * 1024 * 1024)
            .addOnSuccessListener(bytes -> {
                try {

                    int bytesRead;

                    inputStream = new ByteArrayInputStream(bytes);

                    while ((bytesRead = inputStream.read(bytes)) > 0) {
                        stream.write(bytes, 0, bytesRead);
                    }

                    inputStream.close();
                    stream.flush();
                    stream.close();
//FINISH

                } catch (IOException e) {
                    closeSession(resolver, uriResolve, e);
                    e.printStackTrace();
                    Crashlytics.logException(e);
                }
            });

        } catch (IOException e) {
            closeSession(resolver, uriResolve, e);
            e.printStackTrace();
            Crashlytics.logException(e);

        }