Related
I need to save an image to camera folder, but as Android Q getExternalStoragePublicDirectory is deprecated, I do it in another way.
What I have (this method receive bitmap and its name):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = mContext.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + IMAGES_FOLDER_NAME);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
OutputStream fos = resolver.openOutputStream(imageUri);
saved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
} else {
String imagesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM).toString() + File.separator + IMAGES_FOLDER_NAME;
File file = new File(imagesDir);
if (!file.exists()) {
file.mkdir();
}
File image = new File(
imagesDir,
name + ".png"
);
final long fileHashCode = image.hashCode();
Logger.d(TAG, "saveImage, saving image file, hashCode = " + fileHashCode);
FileOutputStream fos = new FileOutputStream(image);
saved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
}
That perfectly works for all needed OS versions, but it looks inaccurate and I'd like to find a more common way.
I tried to play around with content values or try some similar way as for Q, but it doesn't work. I've seen many questions here, but neither of them can help me.
The question is how can I optimize saving for OS lower than Q?
The most generalized version I was able to write is:
private Uri saveImage(Context context, Bitmap bitmap, #NonNull String folderName, #NonNull String fileName) throws IOException {
OutputStream fos = null;
File imageFile = null;
Uri imageUri = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + folderName);
imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
if (imageUri == null)
throw new IOException("Failed to create new MediaStore record.");
fos = resolver.openOutputStream(imageUri);
} else {
File imagesDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).toString() + File.separator + folderName);
if (!imagesDir.exists())
imagesDir.mkdir();
imageFile = new File(imagesDir, fileName + ".png");
fos = new FileOutputStream(imageFile);
}
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos))
throw new IOException("Failed to save bitmap.");
fos.flush();
} finally {
if (fos != null)
fos.close();
}
if (imageFile != null) {//pre Q
MediaScannerConnection.scanFile(context, new String[]{imageFile.toString()}, null, null);
imageUri = Uri.fromFile(imageFile);
}
return imageUri;
}
If you've found a better way, post here, I'll mark it as answer.
Using this document : https://developer.android.com/training/data-storage
Create temp file first
val mTempFileRandom = Random()
fun createTempFile(ext:String, context:Context):String {
val path = File(context.getExternalCacheDir(), "AppFolderName")
if (!path.exists() && !path.mkdirs())
{
path = context.getExternalCacheDir()
}
val result:File
do
{
val value = Math.abs(mTempFileRandom.nextInt())
result = File(path, "AppFolderName-" + value + "-" + ext)
}
while (result.exists())
return result.getAbsolutePath()
}
Send file from path
copyFileToDownloads(this#CameraNewActivity, File(savedUri.path))
Copy data to storage
MAIN_DIR: Main folder name in which you want to store image (Like App Name)
IMAGE_DIR: Sub folder if you want to create.
fun copyFileToDownloads(context: Context, downloadedFile: File): Uri? {
// Create an image file name
val timeStamp = SimpleDateFormat(DATE_FORMAT_SAVE_IMAGE).format(Date())
val imageFileName = "JPEG_$timeStamp.jpg"
val resolver = context.contentResolver
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFileName)
put(MediaStore.Images.Media.MIME_TYPE, IMAGE_MIME_TYPE)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + MAIN_DIR + File.separator + IMAGE_DIR + File.separator)
}
resolver.run {
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri
}
} else {
val authority = "${context.packageName}.provider"
val imagePath =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)?.absolutePath
val destinyFile = File(imagePath, imageFileName)
val uri = FileProvider.getUriForFile(context, authority, destinyFile)
FileUtils.scanFile(context, destinyFile.absolutePath)
uri
}?.also { uri ->
var writtenValue = 0L
// Opening an outputstream with the Uri that we got
resolver.openOutputStream(uri)?.use { outputStream ->
downloadedFile.inputStream().use { inputStream ->
writtenValue = inputStream.copyTo(outputStream)
Log.d("Copy Written flag", " = $writtenValue")
}
}
}
}
ScanFile : ( More detail : https://developer.android.com/reference/android/media/MediaScannerConnection )
fun scanFile(context:Context, path:String) {
MediaScannerConnection.scanFile(context,
arrayOf<String>(path), null,
{ newPath, uri-> if (BuildConfig.DEBUG)
Log.e("TAG", "Finished scanning " + newPath) })
}
Can't we use media store for above and below Android Q?
I have tried following way and it is working fine.
private fun writeImage() {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val imageDetail = ContentValues().apply {
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "${System.currentTimeMillis()}.jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val contentUri = contentResolver.insert(uri, imageDetail)
contentUri?.let {
contentResolver.openFileDescriptor(contentUri, "w", null).use { pd ->
pd?.let {
/* val fos = FileOutputStream(it.fileDescriptor)
val array = getBitmapToBase64()
fos.write(array, 0, array.size)
fos.close()
*/
// or
// Your logic to write an Image file.
}
}
imageDetail.clear()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageDetail.put(MediaStore.Images.Media.IS_PENDING, 0)
contentUri.let { contentResolver.update(it, imageDetail, null, null) }
}
// open the saved image file with gallery app
Snackbar.make(
findViewById(android.R.id.content), "saved", Snackbar.LENGTH_LONG
).setAction("Show") {
val intent = Intent(Intent.ACTION_VIEW, contentUri)
startActivity(intent)
}.show()
} ?: Toast.makeText(this, "not saved", Toast.LENGTH_LONG).show()
}
This is the current code I have to store videos generated by my app on disk. I want this videos to be accessible by other apps like TikTok, WhatsApp and google Photos. I also want to create my own gallery inside my app. However, the files saved by approach below do not show up on TikTok. They show up under Google photos Library\Movies folder, which users have to really dig deep to find.
How to make these files accessible to all apps? how can I write my own video gallery to just show files created by my app?
private void addVideoToGallery(File videoFile) {
Uri uriSavedVideo;
File createdvideo = null;
ContentResolver resolver = activity.getContentResolver();
String path = videoFile.getAbsolutePath();
String videoFileName = path.substring(path.lastIndexOf("/") + 1);
ContentValues valuesvideos;
valuesvideos = new ContentValues();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + File.pathSeparator + activity.getString(R.string.app_name));
valuesvideos.put(MediaStore.Video.Media.TITLE, videoFileName);
valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
Uri collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
uriSavedVideo = resolver.insert(collection, valuesvideos);
} else {
String directory = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + Environment.DIRECTORY_MOVIES;
createdvideo = new File(directory, videoFileName);
valuesvideos.put(MediaStore.Video.Media.TITLE, videoFileName);
valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
valuesvideos.put(MediaStore.Video.Media.DATA, createdvideo.getAbsolutePath());
uriSavedVideo = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, valuesvideos);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
}
ParcelFileDescriptor pfd;
try {
pfd = resolver.openFileDescriptor(uriSavedVideo, "w");
FileOutputStream out = new FileOutputStream(pfd.getFileDescriptor());
FileInputStream in = new FileInputStream(videoFile);
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.close();
in.close();
pfd.close();
} catch (Exception e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
valuesvideos.clear();
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(uriSavedVideo, valuesvideos, null, null);
}
}
This code cannot access the same files from my app using code below.
private void getVids(){
List<Video> videoList = new ArrayList<Video>();
Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
String[] projection = new String[] {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
" >= ?";
String[] selectionArgs = new String[] {
String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES))};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";
try (Cursor cursor = activity.getContentResolver().query(
collection,
projection,
selection,
selectionArgs,
sortOrder
)) {
// Cache column indices.
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
int durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);
while (cursor.moveToNext()) {
// Get values of columns for a given video.
long id = cursor.getLong(idColumn);
String name = cursor.getString(nameColumn);
int duration = cursor.getInt(durationColumn);
int size = cursor.getInt(sizeColumn);
Uri contentUri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList.add(new Video(contentUri, name, duration, size));
}
}
}
unfortunately the solutions I've found didn't work on android 5.1.1.
I have a bitmap called source. I need to save it directly to my phone's gallery. My manifest contains <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Can you give me a working method to do this?
There were several different ways to do it before API 29 (Android Q) but all of them involved one or a few APIs that are deprecated with Q. In 2019, here's a way to do it that is both backward and forward compatible:
(And since it is 2019 so I will write in Kotlin)
/// #param folderName can be your app's name
private fun saveImage(bitmap: Bitmap, context: Context, folderName: String) {
if (android.os.Build.VERSION.SDK_INT >= 29) {
val values = contentValues()
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + folderName)
values.put(MediaStore.Images.Media.IS_PENDING, true)
// RELATIVE_PATH and IS_PENDING are introduced in API 29.
val uri: Uri? = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
saveImageToStream(bitmap, context.contentResolver.openOutputStream(uri))
values.put(MediaStore.Images.Media.IS_PENDING, false)
context.contentResolver.update(uri, values, null, null)
}
} else {
val directory = File(Environment.getExternalStorageDirectory().toString() + separator + folderName)
// getExternalStorageDirectory is deprecated in API 29
if (!directory.exists()) {
directory.mkdirs()
}
val fileName = System.currentTimeMillis().toString() + ".png"
val file = File(directory, fileName)
saveImageToStream(bitmap, FileOutputStream(file))
if (file.absolutePath != null) {
val values = contentValues()
values.put(MediaStore.Images.Media.DATA, file.absolutePath)
// .DATA is deprecated in API 29
context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
}
}
}
private fun contentValues() : ContentValues {
val values = ContentValues()
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png")
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
return values
}
private fun saveImageToStream(bitmap: Bitmap, outputStream: OutputStream?) {
if (outputStream != null) {
try {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
Also, before calling this, you need to have WRITE_EXTERNAL_STORAGE first.
Use this one:
private void saveImage(Bitmap finalBitmap, String image_name) {
String root = Environment.getExternalStorageDirectory().toString();
File myDir = new File(root);
myDir.mkdirs();
String fname = "Image-" + image_name+ ".jpg";
File file = new File(myDir, fname);
if (file.exists()) file.delete();
Log.i("LOAD", root + fname);
try {
FileOutputStream out = new FileOutputStream(file);
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Use this code this we help you to store images into a particular folder which is saved_images and that folder images show in gallery immediately.
private void SaveImage(Bitmap finalBitmap) {
String root = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).toString();
File myDir = new File(root + "/saved_images");
myDir.mkdirs();
Random generator = new Random();
int n = 10000;
n = generator.nextInt(n);
String fname = "Image-"+ n +".jpg";
File file = new File (myDir, fname);
if (file.exists ()) file.delete ();
try {
FileOutputStream out = new FileOutputStream(file);
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
// sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
// Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
// Tell the media scanner about the new file so that it is
// immediately available to the user.
MediaScannerConnection.scanFile(this, new String[]{file.toString()}, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
Log.i("ExternalStorage", "Scanned " + path + ":");
Log.i("ExternalStorage", "-> uri=" + uri);
}
});
}
From Android Q there are changes in saving image to gallery.Thanks to #BaoLei, here is my answer in java if anybody needs it.
private void saveImage(Bitmap bitmap) {
if (android.os.Build.VERSION.SDK_INT >= 29) {
ContentValues values = contentValues();
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + getString(R.string.app_name));
values.put(MediaStore.Images.Media.IS_PENDING, true);
Uri uri = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try {
saveImageToStream(bitmap, this.getContentResolver().openOutputStream(uri));
values.put(MediaStore.Images.Media.IS_PENDING, false);
this.getContentResolver().update(uri, values, null, null);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
} else {
File directory = new File(Environment.getExternalStorageDirectory().toString() + '/' + getString(R.string.app_name));
if (!directory.exists()) {
directory.mkdirs();
}
String fileName = System.currentTimeMillis() + ".png";
File file = new File(directory, fileName);
try {
saveImageToStream(bitmap, new FileOutputStream(file));
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
private ContentValues contentValues() {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
}
return values;
}
private void saveImageToStream(Bitmap bitmap, OutputStream outputStream) {
if (outputStream != null) {
try {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Here is a fully working solution in Kotlin:
fun saveToGallery(context: Context, bitmap: Bitmap, albumName: String) {
val filename = "${System.currentTimeMillis()}.png"
val write: (OutputStream) -> Boolean = {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DCIM}/$albumName")
}
context.contentResolver.let {
it.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)?.let { uri ->
it.openOutputStream(uri)?.let(write)
}
}
} else {
val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString() + File.separator + albumName
val file = File(imagesDir)
if (!file.exists()) {
file.mkdir()
}
val image = File(imagesDir, filename)
write(FileOutputStream(image))
}
}
Do it in One Line
MediaStore.Images.Media.insertImage(applicationContext.getContentResolver(), IMAGE ,"nameofimage" , "description");
Now we have android10 and android11, so here is an updated version, that will work in all android devices.
Make sure you have the WRITE_EXTERNAL_STORAGE permission before calling this function.
private fun saveMediaToStorage(bitmap: Bitmap) {
val filename = "${System.currentTimeMillis()}.jpg"
var fos: OutputStream? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentResolver?.also { resolver ->
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val imageUri: Uri? =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
fos = imageUri?.let { resolver.openOutputStream(it) }
}
} else {
val imagesDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val image = File(imagesDir, filename)
fos = FileOutputStream(image)
}
fos?.use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
toast("Saved to Photos")
}
}
Its kotlin, and I think very straight forward and self explaining code. But still if you have a problem, comment below and I will explain.
Reference: Android Save Bitmap to Gallery Tutorial.
I'd like to add Java code based on #Bao Lei 's answer that I used in my app.
private void saveImage(Bitmap bitmap, Context context, String folderName) throws FileNotFoundException {
if (android.os.Build.VERSION.SDK_INT >= 29) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/" + folderName);
values.put(MediaStore.Images.Media.IS_PENDING, true);
// RELATIVE_PATH and IS_PENDING are introduced in API 29.
Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
saveImageToStream(bitmap, context.getContentResolver().openOutputStream(uri));
values.put(MediaStore.Images.Media.IS_PENDING, false);
context.getContentResolver().update(uri, values, null, null);
}
} else {
dir = new File(getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),"");
// getExternalStorageDirectory is deprecated in API 29
if (!dir.exists()) {
dir.mkdirs();
}
java.util.Date date = new java.util.Date();
imageFile = new File(dir.getAbsolutePath()
+ File.separator
+ new Timestamp(date.getTime()).toString()
+ "Image.jpg");
imageFile = new File(dir.getAbsolutePath()
+ File.separator
+ new Timestamp(date.getTime()).toString()
+ "Image.jpg");
saveImageToStream(bitmap, new FileOutputStream(imageFile));
if (imageFile.getAbsolutePath() != null) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, imageFile.getAbsolutePath());
// .DATA is deprecated in API 29
context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
}
}
private ContentValues contentValues() {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
return values;
}
private void saveImageToStream(Bitmap bitmap, OutputStream outputStream) {
if (outputStream != null) {
try {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
This one worked fine in my app.
For Media Scanning, you can simply do
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
intent.data = Uri.fromFile(path) // path must be of File type
context.sendBroadcast(intent)
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?
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.
==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);
}
I need to save an image to camera folder, but as Android Q getExternalStoragePublicDirectory is deprecated, I do it in another way.
What I have (this method receive bitmap and its name):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = mContext.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + IMAGES_FOLDER_NAME);
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
OutputStream fos = resolver.openOutputStream(imageUri);
saved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
} else {
String imagesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM).toString() + File.separator + IMAGES_FOLDER_NAME;
File file = new File(imagesDir);
if (!file.exists()) {
file.mkdir();
}
File image = new File(
imagesDir,
name + ".png"
);
final long fileHashCode = image.hashCode();
Logger.d(TAG, "saveImage, saving image file, hashCode = " + fileHashCode);
FileOutputStream fos = new FileOutputStream(image);
saved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
}
That perfectly works for all needed OS versions, but it looks inaccurate and I'd like to find a more common way.
I tried to play around with content values or try some similar way as for Q, but it doesn't work. I've seen many questions here, but neither of them can help me.
The question is how can I optimize saving for OS lower than Q?
The most generalized version I was able to write is:
private Uri saveImage(Context context, Bitmap bitmap, #NonNull String folderName, #NonNull String fileName) throws IOException {
OutputStream fos = null;
File imageFile = null;
Uri imageUri = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + folderName);
imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
if (imageUri == null)
throw new IOException("Failed to create new MediaStore record.");
fos = resolver.openOutputStream(imageUri);
} else {
File imagesDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).toString() + File.separator + folderName);
if (!imagesDir.exists())
imagesDir.mkdir();
imageFile = new File(imagesDir, fileName + ".png");
fos = new FileOutputStream(imageFile);
}
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos))
throw new IOException("Failed to save bitmap.");
fos.flush();
} finally {
if (fos != null)
fos.close();
}
if (imageFile != null) {//pre Q
MediaScannerConnection.scanFile(context, new String[]{imageFile.toString()}, null, null);
imageUri = Uri.fromFile(imageFile);
}
return imageUri;
}
If you've found a better way, post here, I'll mark it as answer.
Using this document : https://developer.android.com/training/data-storage
Create temp file first
val mTempFileRandom = Random()
fun createTempFile(ext:String, context:Context):String {
val path = File(context.getExternalCacheDir(), "AppFolderName")
if (!path.exists() && !path.mkdirs())
{
path = context.getExternalCacheDir()
}
val result:File
do
{
val value = Math.abs(mTempFileRandom.nextInt())
result = File(path, "AppFolderName-" + value + "-" + ext)
}
while (result.exists())
return result.getAbsolutePath()
}
Send file from path
copyFileToDownloads(this#CameraNewActivity, File(savedUri.path))
Copy data to storage
MAIN_DIR: Main folder name in which you want to store image (Like App Name)
IMAGE_DIR: Sub folder if you want to create.
fun copyFileToDownloads(context: Context, downloadedFile: File): Uri? {
// Create an image file name
val timeStamp = SimpleDateFormat(DATE_FORMAT_SAVE_IMAGE).format(Date())
val imageFileName = "JPEG_$timeStamp.jpg"
val resolver = context.contentResolver
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFileName)
put(MediaStore.Images.Media.MIME_TYPE, IMAGE_MIME_TYPE)
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + File.separator + MAIN_DIR + File.separator + IMAGE_DIR + File.separator)
}
resolver.run {
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri
}
} else {
val authority = "${context.packageName}.provider"
val imagePath =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)?.absolutePath
val destinyFile = File(imagePath, imageFileName)
val uri = FileProvider.getUriForFile(context, authority, destinyFile)
FileUtils.scanFile(context, destinyFile.absolutePath)
uri
}?.also { uri ->
var writtenValue = 0L
// Opening an outputstream with the Uri that we got
resolver.openOutputStream(uri)?.use { outputStream ->
downloadedFile.inputStream().use { inputStream ->
writtenValue = inputStream.copyTo(outputStream)
Log.d("Copy Written flag", " = $writtenValue")
}
}
}
}
ScanFile : ( More detail : https://developer.android.com/reference/android/media/MediaScannerConnection )
fun scanFile(context:Context, path:String) {
MediaScannerConnection.scanFile(context,
arrayOf<String>(path), null,
{ newPath, uri-> if (BuildConfig.DEBUG)
Log.e("TAG", "Finished scanning " + newPath) })
}
Can't we use media store for above and below Android Q?
I have tried following way and it is working fine.
private fun writeImage() {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val imageDetail = ContentValues().apply {
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "${System.currentTimeMillis()}.jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val contentUri = contentResolver.insert(uri, imageDetail)
contentUri?.let {
contentResolver.openFileDescriptor(contentUri, "w", null).use { pd ->
pd?.let {
/* val fos = FileOutputStream(it.fileDescriptor)
val array = getBitmapToBase64()
fos.write(array, 0, array.size)
fos.close()
*/
// or
// Your logic to write an Image file.
}
}
imageDetail.clear()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageDetail.put(MediaStore.Images.Media.IS_PENDING, 0)
contentUri.let { contentResolver.update(it, imageDetail, null, null) }
}
// open the saved image file with gallery app
Snackbar.make(
findViewById(android.R.id.content), "saved", Snackbar.LENGTH_LONG
).setAction("Show") {
val intent = Intent(Intent.ACTION_VIEW, contentUri)
startActivity(intent)
}.show()
} ?: Toast.makeText(this, "not saved", Toast.LENGTH_LONG).show()
}