1

I was able to download a file from Firebase Storage to storage/emulated/0/Pictures which is a default folder for picture that is being used by most popular app as well such as Facebook or Instagram. Now that Android Q has a lot of behavioral changes in storing and accessing a file, my app no longer be able to download a file from the bucket when running in Android Q.

This is the code that write and download the file from the Firebase Storage bucket to a mass storage default folders like Pictures, Movies, Documents, etc. It works on Android M but on Q it will not work.

    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)

);

With this it will download the files from server to your device storage with path storage/emulated/0/Pictures/MY_APP_NAME but with Android Q this will no longer work as many APIs became deprecated like Environment.getExternalStorageDirectory().

Using android:requestLegacyExternalStorage=true is a temporary solution but will no longer work soon on Android 11 and above.

So my question is how can I download files using Firebase Storage APIs on default Picture or Movie folder that is in the root instead of Android/data/com.myapp.package/files.

Does MediaStore and ContentResolver has solution for this? What changes do I need to apply?

Mihae Kheel
  • 2,441
  • 3
  • 14
  • 38
  • You can download as Uri. After convert uri to bitmap. Then save your file to "storage/emulated/0/Pictures/MY_APP_NAME" using MediaStore and ContentResolver. – Kasım Özdemir Apr 02 '20 at 19:03
  • Or if you know the file url you can download file as bitmap using Glide. Then save your file to "storage/emulated/0/Pictures/MY_APP_NAME. You can download your file to external storage , no problem. – Kasım Özdemir Apr 02 '20 at 19:13
  • `Does MediaStore and ContentResolver has solution for this? `. Yes. And code has been published many times the last weeks. If only you would read pages tagged `mediastore'. – blackapps Apr 02 '20 at 19:29
  • you want to download it here "storage/emulated/0/Pictures", right? – Kasım Özdemir Apr 02 '20 at 19:42
  • @KasımÖzdemir yes but what if it is a video or pdf not just image? Can you show me how to handle the return Uri? – Mihae Kheel Apr 03 '20 at 04:04
  • @blackapps I already spent searching for a couple of hours but it seems there is no yet answer to this kind of question. As you can see here the the app need to download a file from Firebase Storage bucket usually storageReference.getFile(file_object_OR_a_uri) is the easiest way because it also provide a .addOnProgressListener() which allows you to monitor the downloaded bytes and total bytes needed to download. But this approach no longer works in Android Q. Using other API such as .getDownloadUrl() or .getBytes(file_size) will not provide you addOnProgressListener() callback – Mihae Kheel Apr 03 '20 at 09:29
  • It seems I am not getting a correct uri needed for the .getFile() method or there is something wrong with the execution. @blackapps please see this https://firebase.google.com/docs/storage/android/download-files#download_to_a_local_file – Mihae Kheel Apr 03 '20 at 09:35
  • Use .getFile() to download to getExternalFilesDir(). After download use the media store and a content resolver to copy the file to the pictures directory. – blackapps Apr 03 '20 at 11:39
  • Pretty strange that there is no StorageReference method which does this right away. Its a class from Google. Or not? – blackapps Apr 03 '20 at 11:41
  • @blackapps or better move it is what I am thinking but is it safe or thread safe? They always make a drastic breaking changes without even providing answer to upcoming issue like this – Mihae Kheel Apr 03 '20 at 11:42
  • @blackapps StorageReference is a class included in Firebase Storage Android SDK com.google.firebase:firebase-storage:x.x.x – Mihae Kheel Apr 03 '20 at 11:43
  • It is unclear how you would do a move. – blackapps Apr 03 '20 at 11:44
  • You may want to test it by yourself by putting a file in Firebase Storage bucke > get its download url from the console itself > then try to download it like what is in the code above on Android lower than Q – Mihae Kheel Apr 03 '20 at 11:46
  • Are you telling that there is a download url? Then you can use HttpUrlConnection to download the files yourself directly to Pictures using the media store. And implement a progressbar. Pretty standard all. I do not understand the fuss. – blackapps Apr 03 '20 at 11:47
  • @blackapps I will try to add more on the code but if you never work with Firebase I'm afraid it will be really unclear to you. – Mihae Kheel Apr 03 '20 at 11:48
  • Yes i do not use Firebase. But if you have a download url then download and save is pretty standard. – blackapps Apr 03 '20 at 11:49
  • @blackapps as what is in the code no need to handle it with HttpUrlConnection as the Firebase Storage dependecy handles it already by StorageReference httpsReference = FirebaseStorage.getInstance().getReferenceFromUrl(download_Url_of_file_from_Firebase_storage); then just call httpsReference.getFile(fileObject).addOnProgressListener(snapShot_where_you_can_listen_to_progress_and_get_bytes ->{ //Do your computation with downloaded bytes and total bytes here }); – Mihae Kheel Apr 03 '20 at 11:50
  • @blackapps I don't think we are in the same page so I add a little code on the and update the question again but this is it all about, how to save the file on default folders such as Pictures, Documents, Movies, Musics with Firebase Storage StorageReference like it used on Android lower than Q – Mihae Kheel Apr 03 '20 at 12:01
  • I also suggest try to work with Firebase Storage first on Android to get a clear idea what is going on if the above question cause confusion. – Mihae Kheel Apr 03 '20 at 12:03
  • You do not have to post code again for lower than Q. I told you how to do it for Q already. – blackapps Apr 03 '20 at 12:05

2 Answers2

1

Here is my solution:

Download file with 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();
}

Get file 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;
}

And save file to external storage

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;
}

save file to stream

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();
    }
}

I tried with emulator Android 29. It works fine.

Note : getExternalStorageDirectory() was deprecated in API level 29. To improve user privacy, direct access to shared/external storage devices is deprecated. When an app targets Build.VERSION_CODES.Q, the path returned from this method is no longer directly accessible to apps. Apps can continue to access content stored on shared/external storage by migrating to alternatives such as Context#getExternalFilesDir(String), MediaStore, or Intent#ACTION_OPEN_DOCUMENT.

Kasım Özdemir
  • 5,414
  • 3
  • 18
  • 35
  • Thanks for the help, so you mean I should use Glide to download files replacing the usage of Firebase Storage? My usage of Firebase Storage here also has progress callback so i can compute the size of file that is being updated and show a progress of it via notification. – Mihae Kheel Apr 03 '20 at 08:34
  • 1
    That url what I set to Glide is already Firebase Storage Url. I just used Glide to download file. You can download as you wish. Important thing is saveFile func. – Kasım Özdemir Apr 03 '20 at 08:38
  • ohh okay okay I am trying to implement it now on base on my requirements thanks will mark this as an answer if it works well. – Mihae Kheel Apr 03 '20 at 09:05
  • So I am replacing the httpsReference.getFile() with httpsReference.getDownloadUrl() the once I get the uri I will use Glide to download it as a regular file right? – Mihae Kheel Apr 03 '20 at 09:08
  • Unfortunately using httpsReference.getDownloadUrl() means I can no longer use httpsReference.addOnProgressListener() meaning I can no longer show the progress bar – Mihae Kheel Apr 03 '20 at 09:15
  • 1
    Url is not not necessary. You can download it as a byte array so using reference. Then you can use byte array instead of file. – Kasım Özdemir Apr 03 '20 at 09:36
  • But I need to listen to the progress of the download with addOnProgressListener() it seems only getFile() is the method providing this callback – Mihae Kheel Apr 03 '20 at 09:39
  • Also Glide is not a network library with a downloader engine might cause huge problem when downloading tons of MBs file – Mihae Kheel Apr 03 '20 at 09:41
  • 1
    I get it. Maybe [this](https://medium.com/@mr.johnnyne/how-to-use-glide-v4-load-image-with-progress-update-eb02671dac18) works for you. – Kasım Özdemir Apr 03 '20 at 10:44
  • Big thanks bro! There is some downside with getByte() but no choice for now, your idea and sample code helps a lot. – Mihae Kheel Apr 03 '20 at 17:19
  • Not at all. If you add progress to glide , it can be as you want. I tried other ways but I could not get any results. – Kasım Özdemir Apr 03 '20 at 17:22
  • Yes but as long as it downloads any file to the default folder which can be seen easily by users I think its okay even though I am no longer able to show the download progress moving on like what can .getFile(file).addOnProgressListener(snapshot ->{}); does I think its acceptable for now. You can check my answer. – Mihae Kheel Apr 03 '20 at 17:28
0

==NEW ANSWER==

If you wanted to monitor the download progress you can use getStream() of FirebaseStorage SDK like this:

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();
            });

==OLD ANSWER==

Finally after tons of hours I manage to do it but using .getBytes(maximum_file_size) instead of .getFile(file_object) as last resort.

Big big thanks to @Kasim for bringing up the idea of getBytes(maximum_file_size) with also sample code working with InputStream and OutputStream.By searching across S.O topic related to I/O also is a big help here and here

The idea here is .getByte(maximum_file_size) will download the file from the bucket and return a byte[] on its addOnSuccessListener callback. The downside is you must specify the file size you allowed to download and no download progress computation can be done AFAIK unless you do some work with outputStream.write(0,0,0); I tried to write it chunk by chunk like here but the solution is throwing ArrayIndexOutOfBoundsException since you must be accurate on working with index into an array.

So here is the code that let you saved file from your Firebase Storage Bucket to your device default directories: 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);

        }
Mihae Kheel
  • 2,441
  • 3
  • 14
  • 38
  • The reason we cant just download it via Glide like what @Kasim propose or any download manager is because Firebase Storage has set of rules where you can edit it and restrict its access depending on what is your requirement. Here one of my basic rules is a user is supposed to be authenticated first before having a read and write access to our bucket. – Mihae Kheel Apr 03 '20 at 17:36