skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

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

  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search