【问题标题】:Firebase Storage download file to Pictures or Movie folder in Android QFirebase 存储将文件下载到 Android Q 中的图片或电影文件夹
【发布时间】:2020-07-14 20:27:26
【问题描述】:

我能够将文件从 Firebase 存储下载到 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 及更高版本。

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

MediaStoreContentResolver 有解决方案吗?我需要应用哪些更改?

【问题讨论】:

  • 您可以下载为 Uri。将uri转换为位图后。然后使用 MediaStore 和 ContentResolver 将文件保存到“storage/emulated/0/Pictures/MY_APP_NAME”。
  • 或者,如果您知道文件 url,您可以使用 Glide 将文件下载为位图。然后将您的文件保存到“storage/emulated/0/Pictures/MY_APP_NAME。您可以将文件下载到外部存储,没问题。
  • Does MediaStore and ContentResolver has solution for this? 。是的。过去几周,代码已多次发布。如果只有你会阅读标记为“媒体商店”的页面。
  • 你想在这里“storage/emulated/0/Pictures”下载,对吧?
  • @KasımÖzdemir 是的,但如果它是视频或 pdf 而不仅仅是图像怎么办?你能告诉我如何处理返回的 Uri 吗?

标签: android firebase firebase-storage android-contentresolver mediastore


【解决方案1】:

这是我的解决方案:

使用 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 中已弃用。为了提高用户隐私,不建议直接访问共享/外部存储设备。当应用程序以 Build.VERSION_CODES.Q 为目标时,从此方法返回的路径不再可供应用程序直接访问。通过迁移到 Context#getExternalFilesDir(String)、MediaStore 或 Intent#ACTION_OPEN_DOCUMENT 等替代方案,应用可以继续访问存储在共享/外部存储上的内容。

【讨论】:

  • 感谢您的帮助,所以您的意思是我应该使用 Glide 下载文件来代替使用 Firebase 存储?我在这里使用 Firebase 存储也有进度回调,因此我可以计算正在更新的文件的大小并通过通知显示它的进度。
  • 我设置为 Glide 的那个 url 已经是 Firebase Storage Url。我只是使用 Glide 下载文件。您可以根据需要下载。重要的是 saveFile 函数。
  • 哦,好吧好吧,我现在正在尝试根据我的要求实施它,谢谢,如果它运作良好,我会将其标记为答案。
  • 网址不是必须的。您可以将其作为字节数组下载,以便使用参考。然后你可以使用字节数组代替文件。
  • 我明白了。也许this 适合你。
【解决方案2】:

==新答案==

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

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 一起使用的示例代码。通过搜索与I/O 相关的SO 主题也是一个很大的帮助herehere

这里的想法是.getByte(maximum_file_size) 将从存储桶中下载文件并在其addOnSuccessListener 回调中返回byte[]。缺点是您必须指定允许下载的文件大小,除非您使用outputStream.write(0,0,0); 进行一些工作,否则无法完成下载进度计算。 @ 因为您必须准确地处理数组中的索引。

下面是让您将文件从 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);

        }

【讨论】:

  • 我们不能像@Kasim 建议的那样通过 Glide 下载它或任何下载管理器的原因是因为 Firebase Storage 有一组规则,您可以在其中编辑它并根据您的要求限制其访问权限。在这里,我的一个基本规则是,在对我们的存储桶进行读写访问之前,应该首先对用户进行身份验证。
猜你喜欢
  • 2020-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多