Reputation: 2651
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?
Upvotes: 1
Views: 2450
Reputation: 2651
==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);
}
Upvotes: 0
Reputation: 5634
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.
Upvotes: 1