I'm building an app that allows the user to save the bitmap or share it without saving it. The 2nd functionality doesn't quite work. I understand that the app needs to save the file to the device before sharing it on a social media app so my idea was, immediately after the file was successfully shared, to automatically delete the file from the device. I've build a delete method trying 2 different approaches and neither have worked:
First approach:
public void deleteFile(String path){
File file = new File(path);
try {
file.getCanonicalFile().delete();
} catch (IOException e) {
e.printStackTrace();
}
}
Second approach:
public void deleteFile(String path){
File file = new File(path);
boolean deleted = file.delete();
}
And I'm calling deleteFile(String) from the sharing method:
public void shareMeme(Bitmap bitmap) {
String path = MediaStore.Images.Media.insertImage(Objects.requireNonNull(getContext()).getContentResolver(), bitmap, "Meme", null);
Uri uri = Uri.parse(path);
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("image/*");
share.putExtra(Intent.EXTRA_STREAM, uri);
share.putExtra(Intent.EXTRA_TEXT, "This is my Meme");
getContext().startActivity(Intent.createChooser(share, "Share Your Meme!"));
deleteFile(path);
}
With respect to your stated problem, insertImage() returns a string representation of a Uri. That Uri is not a file. Calling getPath() on it is pointless, and you cannot delete anything based on that path.
More broadly, if your intention is to delete the content right away:
Do not put it in the MediaStore
Do not share it, as you will be deleting it before the other app has a chance to do anything with it
If you want to share it, but then delete it:
Do not put it in the MediaStore
Delete it the next day, or in a few hours, or something, as you have no good way of knowing when the other app is done with the content
To share an image with another app without using the MediaStore:
Save the image to a file in getCacheDir() (call that on a Context, such as an Activity or Service)
Use FileProvider to make that file available to other apps
Beyond that:
Do not use wildcard MIME types in ACTION_SEND. You are the one who is supplying the content to send. You know the actual MIME type. Use it.
Note that there is no requirement for an ACTION_SEND activity to honor both EXTRA_TEXT and EXTRA_STREAM. Most seem to do so, but that behavior is outside of the ACTION_SEND specification.
Note that insertImage() is deprecated on Android Q.
First, you need to check if your file exists, (maybe you set the wrong path?). Then delete the file
File file = new File(path);
if (file.exists()){
if (file.delete()) {
Toast.makeText(this, "file Deleted :" + path, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "file not Deleted :" + path, Toast.LENGTH_SHORT).show();
}
}
Related
My goal is to:
Save media file to External Storage (in my case it's photo).
Get file path or URI of saved data.
Save it to SQLite (either file path or content URI or smth else).
Be able to get correct URI to this content at any point in the future.
It's very similar to what other very popular application do - they create their directory in 'Pictures' folder and store there photos and use them in their applications while they're also available for viewing using gallery/file explorer etc.
As I understand recommended way to save media content (image, f.e.) is to use MediaStore API and as a result I get content URI, which I can use later.
But then I read that these content URIs might be changed after re-scan of Media happens, so it looks it's not reliable. (For example if SD card is used and it's taken out and inserted again)
At the same time usage of absolute file paths is not recommended and there's tendency to deprecate APIs which use absolute file paths to work with External Storage. So it doesn't look reliable either.
I can only imagine the following solution:
Use unique auto-generated file name while saving (like UUID).
When I need to get content URI (f.e. want to render photo in ImageView) - I can use ContentResolver and search for content URI using file name filter.
Problem with this approach is that I have a lot of photos (gallery) and querying it using ContentResolver can affect performance significantly.
I feel like I'm over complicating things and missing something.
You are indeed overcomplicating things.
Store file to the needed folder in the filesystem(it is better to name the folder under your app name)
Store this path or URI path - whatever you like. (Do not hardcode passes though in your app - device vendors may have different base paths in their devices)
As long as the folder is named the same and files in it named the same(as in your db) - you will be able to access them even if the sdcard was taken out and then put back in.
There are possible complications after reindexing - but for the eight years I work as Android dev I encountered it only once, thus you can easily ignore this stuff.
If you want to have more control over what you store and want to limit access to it - store data into the inner storage of your app - this way you will be 100% sure of where the data is and that it is not tampered with.
Starting from Android 10 you have scoped storage - it is like internal storage but it may be even on an external sdcard.
Here is a small overview of possible storage locations.
And don't overthink it too much - it is a default usecase of the phone and it works just as you would expect - pretty ok and pretty stable.
first, you have to apply for external storage permission in manifest and Runtime Permission Also.
after creating a directory for saving an image in this directory.
you have to also add file provider in XML and code side because it's required.
now it's time to code check my code for saving an image in the folder also these image in the gallery and get the path from a file path.
convert URI to bitmap
http://prntscr.com/10dpvjj
save image function from getting bitmap
private String save(Bitmap bitmap) {
File save_path = null;
if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {
try {
File sdCard = Environment.getExternalStorageDirectory();
File dir = new File(sdCard.getAbsolutePath() + "/SaveDirectory");
dir.mkdirs();
File file = new File(dir, "DirName_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()) + ".png");
save_path = file;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
FileOutputStream f = null;
f = new FileOutputStream(file);
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);
if (f != null) {
f.write(baos.toByteArray());
f.flush();
f.close();
}
} catch (Exception e) {
// TODO: handle exception
}
Share(save_path); // call your Function Store into database
Log.e("PathOFExec----", "save: " + save_path);
}
get store image location into your database if you wish
private void Share(File savePath) {
if (savePath != null) {
Uri uri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", savePath);
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("image/*");
share.putExtra(Intent.EXTRA_TEXT, "TextDetail");
share.putExtra(Intent.EXTRA_STREAM, uri);
context.startActivity(Intent.createChooser(share, "Share Image!"));
//after getting URI you can store the image into SQLite databse for get uri
}
}
I would recommend using Intent.ACTION_OPEN_DOCUMENT for your demand.
1. Create Photo Picking Intent:
val REQUEST_CODE_PICK_PHOTO = 1
fun pickAndSavePhoto(requestCode: Int) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "image/*"
startActivityForResult(intent, requestCode)
}
2. Handle Result:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_PICK_PHOTO && resultCode == RESULT_OK) {
val imageUri = data!!.data!!
//save this uri to your database as String -> imageUri.toString()
}
}
3. Get Image back and Display on ImageView:
fun getBitmapFromUri(context: Context, imageUri: Uri): Bitmap? { //uri is just an address, image may be deleted any time, if so returns null
val bitmap: Bitmap
return try {
val inputStream = context.contentResolver.openInputStream(imageUri)
inputStream.use {
bitmap = BitmapFactory.decodeStream(it)
}
bitmap
} catch (e: Exception) {
Log.e("getBitmapFromUri()", "Image not found.")
null
}
}
val bitmap = getBitmapFromUri(context, imageUri) //get uri String from database and convert it to uri -> uriString.toUri()
if (bitmap != null) {
imageView.setImageBitmap(bitmap)
}
Only ACTION_OPEN_DOCUMENT can access file uri permanently:
Android Retrieve Image by Intent Uri Failed: "has no access to content..."
Demo: https://www.youtube.com/watch?v=LFfWnt77au8
Recently I am developing a file sharing application and I created a GridView, where the downloaded files are being shown. From this View, I would like to be able to open the default application through an intent, to open the whole file. Currently I am testing the app with only image files. All the files are downloaded to the external public directory this way:
File externalFolder = new File(Environment.getExternalStorageDirectory(), "My application");
if(!externalFolder.exists()){
externalFolder.mkdir();
}
...
File folder = new File(externalFolder, "Images");
if(!folder.exists()){
folder.mkdir();
}
...
String filename = folder.getAbsolutePath() + "/" + fileToDownload.getName() + "." + fileToDownload.getExtension();
FileOutputStream fos = new FileOutputStream(filename);
When the file is downloaded, I scan it with MediaScannerConnection.scanFile. The scan is successful, the picture is visible among other files in Photos app.
After the file is downloaded, I am able to extract a thumbnail in the adapter of the GridView, so I surely have a valid path to the file.
And where the fun begins: I tried to set an onClickListener to the GridView items in the adapter to be able to open the picture in Photos app this way:
listItem.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if(new File(current.getPath()).exists()){
Log.e("path", "valid");
}else{
Log.e("path", "invalid");
}
Log.e("path", Uri.parse("content://"+current.getPath()).toString());
intent.setDataAndType(Uri.parse("content://"+current.getPath()), "image/*");
getContext().startActivity(intent);
}
});
The Intent is created successfully, I get the following in the log:
E/path: valid
E/path: content:///storage/emulated/0/My application/Images/best_picture_ever.jpeg
I have the option to choose among apps to open. When I select the app, it fails to open the image, like when it does not exist. All the 5 applications.
I tested this on my device with Oreo, and on two emulated devices with Nougat and Lollipop, all of them behaves the same way.
What am I doing wrong?
What am I doing wrong?
You are not creating a valid Uri. You cannot put content:// in front of arbitrary things and have a useful Uri, any more than you can put https:// in front of arbitrary things and have a usable URL.
Use FileProvider to serve up this file.
I have an application in which I can use the device's camera to take a picture. What I would like to do is to start the ACTION_IMAGE_CAPTURE intent without assigning an EXTRA_OUTPUT, and then move the file that is created in the default location to my own custom location using file.renameTo. My code is something like this:
/* Start camera activity without EXTRA_OUTPUT */
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, _REQUESTCODE_ATTACH_CAMERA);
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch(requestCode) {
case _REQUESTCODE_ATTACH_CAMERA:
/* Get path to most recently added image */
final String[] imageColumns = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA };
final String imageOrderBy = MediaStore.Images.Media._ID + " DESC";
Cursor imageCursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageColumns, null, null, imageOrderBy);
String fullPath = "";
if(imageCursor.moveToFirst()){
fullPath = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
imageCursor.close();
}
File f = Environment.getExternalStorageDirectory();
f = new File(f.getAbsolutePath() + File.separator + "DCIM" + File.separator + MY_APP_NAME;
if(!f.exists()) {
f.mkdirs();
}
/* Create new file based on name of most recently created image */
File oldFile = new File(fullPath);
String newPath = f.getAbsolutePath() + File.separator + oldFile.getName() ;
/* Move file with renameTo */
oldFile.renameTo(new File(newPath));
break;
...
}
}
}
All of this works quite well, however there is one strange thing that is occurring. In my app, I have another button that allows selecting an existing image from the phone's gallery. That code looks like this:
Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
activity.startActivityForResult(galleryIntent, _REQUESTCODE_ATTACH_GALLERY);
This also works, but if I take a picture with the camera using the code posted above, and then try to select another image from the gallery, there will be blank "broken link" type items in the gallery that contain no content and are unselectable. These seem to correspond with photos taken and moved using renameTo; if I put in code in onActivityResult to post the filename to LogCat, the name that gets logged is the same as the name of the previously moved file that it corresponds to. Trying to create a File object or in any way access that filename, results in null objects and force closes.
The strange part is that there is no evidence of these "broken link" files in Eclipse DDMS, nor in the phone itself if I use Root Browser, and they disappear if I remount the SD Card.
The whole reason I am moving the images after capturing them with the camera is to avoid filling up the phone's gallery storage with unnecessary images. While these empty "broken link" type files don't appear to be taking up any storage space, they would still be very annoying to an end-user trying to browse through their gallery. Does anyone have any ideas on what is happening here or how to solve this problem?
EDIT:
Here is a photo showing what the gallery looks like with a "broken link" type image displayed. One of these will appear for every photo that is taken using my app, and they will all disappear if I remount the SD Card.
Thanks in part to this SO thread, I have discovered a solution. It actually makes sense that it would behave this way since there is a table kept for media content and so removing something without telling the table would definitely create a "broken link" type scenario.
The ultimate solution is to use contentResolver.delete to remove the reference to the file within the content resolver, but there are two different ways that I have found that will work.
/* Moving with renameTo */
//Use the same exact code as I had before (shortened for brevity) to move the file
oldFile.renameTo(newFile);
//Get URI from contentResolver using file Id from cursor
Uri oldUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media._ID)));
//Delete old file
getContentResolver().delete(oldUri, null, null);
Getting the URI in this way is necessary because it requires a reference to the image in the contentResolver rather than the path to its location in storage. This way might feel dirty to some since you are moving a file and then calling a delete function on that file in order to sort of trick the content resolver into removing the link to the file. If you would rather, you can do it without using renameTo so that the call to delete(...) actually does delete the image.
/* Moving with streams */
//Get streams
InputStream in = new FileInputStream(oldFile);
OutputStream out = new FileOutputStream(newFile);
byte[] buffer = new byte[1024];
int bytesRead = 0;
//Read old file into new file
while((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
//Get URI from contentResolver using file Id from cursor
Uri oldUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media._ID)));
//Delete old file
getContentResolver().delete(oldUri, null, null);
The call to contentResolver.delete is the same either way, I just wanted to point out that it will still work if the image has already been removed.
During this I discovered a solution to a problem that I didn't even realize that I had that I will post here as well in case anyone with this same problem comes across this in the future. In order to keep the image as selectable in the device gallery from the new location, you need to let the media scanner know that a change has been made. There are two ways that I found to do this:
/* This is the only way that I know of to handle multiple new files at once. I
really would use this sparingly, however, since it will rescan the entire
SD Card. Not only could this take a long time if the user has a lot of files
on their card, it will also show a notification so it is not exactly a
transparent operation. */
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" + Environment.getExternalStorageDirectory())));
/* You *could* do multiple files with this by passing in the path for each one
in the array of Strings, however an instance of this will get called for each
one rather than it doing them all at once. Likewise, your onScanCompleted
(if you choose to include one) will get called once for each file in the list.
So really, while this is much better for a small number of files, if you plan
on scanning a very large amount then the full rescan above would probably be
a better option. */
MediaScannerConnection.scanFile(context, new String[]{ newFilePathAsString }, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
//This executes when scanning is completed
}
}
);
So I have these buttons in my app that I want to open .PDF files from within the application once pressed.
I've stored my files in the /res/raw directory. Let's call them file1.PDF, file2.PDF etc.
public void onClick(View v) {
switch (v.getId()) {
case R.id.file1:
Log.i(TAG, "Button one pressed");
openPDF(Uri.parse("android:resource://test.pdf.files/raw/res/file1.PDF"));
break;
}}
Above is my code for the onClick. Underneath is the code for the openPDF function.
public void openPDF(Uri url)
{
File file = new File(url.toString());
Log.i(TAG, url.toString());
if (file.exists())
{
Log.i(TAG, "File exists");
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(file), "application/pdf");
try
{
getApplicationContext().startActivity(intent);
}
catch (ActivityNotFoundException e)
{
Log.e(TAG,"Activity not found exception");
}
}else{
Log.i(TAG,"404: File not found!");
}
}
My problem is as follows, I can never get past the if (file.exists()) check. I've tried making the File file take both Uri, URI and String, I have also tried more or less every variation of the filepath, like "res/raw/file1.PDF", "raw/file.PDF", "test.pdf.files/raw/res/file1.PDF". But in any of these cases I just can't find the file I want to open, can anyone see why this won't work?
I have been trying to fix this for hours and hours and I just can't seem to wrap my head around it.
I can never get past the if (file.exists()) check
Of course. Your Uri does not point to a file. Your Uri points to a resource. Resources are not files. Files are not resources.
You can try startActivity() with an Intent that uses your resource Uri. If you find that PDF viewers refuse to work with that Uri, you will need to copy the resource to a local world-readable file (e.g., on external storage) and use that copy with Uri.fromFile() to launch your PDF viewer.
I got this class:
import android.content.Context;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.util.Log;
public class MediaScannerWrapper implements
MediaScannerConnection.MediaScannerConnectionClient {
private MediaScannerConnection mConnection;
private String mPath;
private String mMimeType;
// filePath - where to scan;
// mime type of media to scan i.e. "image/jpeg".
// use "*/*" for any media
public MediaScannerWrapper(Context ctx, String filePath, String mime){
mPath = "/sdcard/DCIM/Camera";
mMimeType = "jpg";
mConnection = new MediaScannerConnection(ctx, this);
}
// do the scanning
public void scan() {
mConnection.connect();
}
// start the scan when scanner is ready
public void onMediaScannerConnected() {
mConnection.scanFile(mPath, mMimeType);
Log.w("MediaScannerWrapper", "media file scanned: " + mPath);
}
public void onScanCompleted(String path, Uri uri) {
// when scan is completes, update media file tags
}
}
How to use it in the other class?
I don't know how to properly use classes, I tried but nothing is working.
I do something wrong, but I don't know what, can someone help me with this.
The Story
Before Android 4.4, we could just send a broadcast to trigger the media scanner on any particular file, or folder or even on the root of the storage. But from 4.4 KitKat, this have been fixed by the Android Developers.
Why do I say fixed? The reason is simple. Sending a broadcast using MEDIA_MOUNTED on the root directory is very expensive. Running the Media Scanner is an expensive operation and the situation gets even worse when the user has got a lot of files in the storage and deep folder structures.
Before Android 4.4
Keep it straight and simple. If you are targeting your app before Android 4.4. But keep in mind not to use it on the root directory unless absolutely necessary.
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" + Environment.getExternalStorageDirectory())));
From Android 4.4
There are two ways for you.
i) The first one is very similar to the previous example, but may not work efficiently and is not recommended too.
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + Environment.getExternalStorageDirectory())));
ii) Now, let us move on to the most recommended and efficient solution to this problem.
Add the file paths of the files which have been updated, like this, in a String type ArrayList
ArrayList<String> toBeScanned = new ArrayList<String>();
toBeScanned.add(item.getFilePath());
Now you need to run scanFile() static method of the MediaScannerConnection class and pass the String array containing the list of all the files which have been updated and needs to be media scanned.
You can also put a listener to respond when the scanning has been finished for individual files.
String[] toBeScannedStr = new String[toBeScanned.size()];
toBeScannedStr = toBeScanned.toArray(toBeScannedStr);
MediaScannerConnection.scanFile(getActivity(), toBeScannedStr, null, new OnScanCompletedListener() {
#Override
public void onScanCompleted(String path, Uri uri) {
System.out.println("SCAN COMPLETED: " + path);
}
});
Hey I found out how to do it with a very simple code.
Just call this line of code:
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" + Environment.getExternalStorageDirectory())));
This should trigger mediascanner.
In Android, there is a content database which is used by the media scanner to keep track of all the media content present on the device.
When Android boots up, the mediascanner service is launched and runs through the entire external storage to find if there is any new media content if it finds one then,
It adds an entry of that media content into the content database
Each entry in the content database contains metadata of the media content like Name, date, file size, type of file, etc..
So when you make a modification to a media content, you will need to update the content database also.
If the content database is not update then other applications also will not be able to access that particular media content.
Running the media scanner just updates the content database
Instead of running the media scanner, you can update the content database yourself and it should resolve the problem.
Here is an explanation on how to insert, delete, update using the content resolver. (Search for the section "Inserting, Updating, and Deleting Data")
Edit:
There is a sample code in this answer. Check for the answer by Janusz.
File file = new File(absolutePath);
Uri uri = Uri.fromFile(file);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri);
sendBroadcast(intent);
private void galleryAddPic() {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
File f = new File(mCurrentPhotoPath);
Uri contentUri = Uri.fromFile(f);
mediaScanIntent.setData(contentUri);
this.sendBroadcast(mediaScanIntent);
}
Reference: http://developer.android.com/training/camera/photobasics.html#TaskGallery
The Add the Photo to a Gallery Section
As #Aritra Roy's answer, i decide to make an experiment about this issue.
What i got here are:
Intent.ACTION_MEDIA_MOUNTED and Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
can accept individual file path, so sendBroadcast(new
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(filePath)));
or sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse(filePath))); will be valid.
If you use individual file path with Intent.ACTION_MEDIA_MOUNTED on Kitkat or above, your application will still crash
If you use Intent.ACTION_MEDIA_SCANNER_SCAN_FILE or MediaScannerConnection on device lower than Kitkat, your application will not force close, but the method will just simply not working as you want.
From that experiment, i think the best method to handle is
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
MediaScannerConnection.scanFile(context, new String[]{imagePath}, null, new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
//something that you want to do
}
});
} else {
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + imagePath)));
}
Let me know if i missed something