Search code examples
androidfirebasefirebase-storageandroid-contentresolvermediastore

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


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?


Solution

  • ==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);
    
            }