This question already has answers here:
Android Kotlin: Getting a FileNotFoundException with filename chosen from file picker?
(5 answers)
Android - Get real path of a .txt file selected from the file explorer
(1 answer)
Closed 1 year ago.
I'm using file picker to let users decide where to extract specific files. After updating Android API, my app is having problem for getting path of exported file whenever users try to extract file into specific folder via file picker because it returns different uri data from before. When user picks folder where Android system allows, my app creates new file, and copies byte data of specific file into that newly created file.
Copying byte data works fine, but after copying, to notice users that extracting process was successful, I show toast message like Extracted 123.png | Downloads/myPicture/123.png. I just got path by splitting uri path with : letter, but now uri path doesn't contain format like before. Below is how I handle extracting
if(result.resultCode == RESULT_OK) {
val data = result.data
if(data != null) {
val file = current
val uri = data.data
if(uri == null || file == null) {
StaticStore.showShortMessage(this, getString(R.string.file_extract_cant))
} else {
val pfd = contentResolver.openFileDescriptor(uri, "w")
if(pfd != null) {
val fos = FileOutputStream(pfd.fileDescriptor)
val ins = file.data.stream
val b = ByteArray(65536)
var len: Int
while(ins.read(b).also { len = it } != -1) {
fos.write(b, 0, len)
}
ins.close()
fos.close()
val path = uri.path
if(path == null) {
StaticStore.showShortMessage(this, getString(R.string.file_extract_semi).replace("_",file.name))
return#registerForActivityResult
}
val f = File(path)
val p = f.absolutePath.split(":")[1]
StaticStore.showShortMessage(this, getString(R.string.file_extract_success).replace("_",file.name).replace("-",p))
} else {
StaticStore.showShortMessage(this, getString(R.string.file_extract_cant))
}
}
}
}
Getting extracted file's name isn't problem, but getting path to newly create file is problem. It worked before updating Android API because f.absolutePath returned real path to created file. I can't remember properly what f.absolutePath returned because it's been long time since I made this, but it contained full path next to : letter, so I just splitted text and got path from there. Now f.absolutePath returns document/357 or like this. Number keeps changing (increasing, especially) whenever I try to extract file like document/358, document/359, etc, but it's surely not path where I decided to export. uri itself also returns format like content://com.android.providers.downloads.documents/document/363 as String
Was there any changes for file picker?
Is there any way to get full path of created file properly?
You're not supposed to try to get a File's path from a Uri. You just use the Uri itself as your reference to the file's location.
Besides, the user selected the location themselves, so they already know where it is.
Related
I am saving my pdf files in Documents Folder or Download Folder. I want to access the file via the primary storage as when I choose the file in the "Downloads" or "Documents", the uri is changed and the file name gets changed to something like msf:63 or document:63
here is the image of when choosing the file in which directory
so my problem is I want to get rid of the "Documents" and the "Downloads" so the user will not choose the file from there. Is that possible?
Here is the code when I try to open the file
Intent(Intent.ACTION_OPEN_DOCUMENT).also {
val mime = arrayOf("application/pdf", //pdf
"application/vnd.openxmlformats-officedocument.wordprocessingml.document") //docx
it.type = "application/*"
it.addCategory(Intent.CATEGORY_OPENABLE)
it.putExtra(Intent.EXTRA_MIME_TYPES, mime)
activityLauncher.launch(it){ its ->
if (its.resultCode == Activity.RESULT_OK){
val str = its.data!!.data!!.path!!.split("/").toTypedArray()
whichNoFileSelected(tag)!!.text = str[str.size - 1]
whichNoFileSelected(tag)!!.textSize = 11f
if(str[str.size - 1].contains(".docx")){
whichNoFileSelected(tag)!!.setCompoundDrawablesWithIntrinsicBounds(
requireContext().resources.getDrawable(R.drawable.ic_doc, null),
null,
null,
null
)
}else{
whichNoFileSelected(tag)!!.setCompoundDrawablesWithIntrinsicBounds(
requireContext().resources.getDrawable(R.drawable.ic_pdf, null),
null,
null,
null
)
}
uploadImage(its.data!!, "document")
}
}
}
I want to get rid of the "Documents" and the "Downloads" so the user will not choose the file from there. Is that possible?
No.
Also, please get rid of this line and the code that depends on it:
val str = its.data!!.data!!.path!!.split("/").toTypedArray()
Use getType() on a ContentResolver to find the MIME type associated with the content that the user chose. Not only will this handle the Documents and Downloads trees, but it will handle all the other Uri scenarios as well (e.g., user chooses a document from Google Drive).
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
I'm building an App that manages audiobook libraries
Using the Intent ACTION_OPEN_DOCUMENT_TREE to let the user choose a Directory as a library, I get an Uri of the form : content:// as a result.
Is there a way to convert the given "content://" Uri to a "file:// filepath" ? ( if that is possible of course )
Or can I tweak the file chooser activity to accept only folders that have an actual file:// path ?
Thank you very much
EDIT : progress !
I managed, using the content resolver, to find a path of the form "1407-1105:Audiobooks" for the SD card, and "primary:Audiobooks" for the main volume. That seems more readable, but I have the same problem still.
Finally found the solution ! Maybe it is a little bit ugly, but that does seems to work !
fun resolveContentUri(uri:Uri): String {
val docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
val docCursor = context.contentResolver.query(docUri, null, null, null, null)
var str:String = ""
// get a string of the form : primary:Audiobooks or 1407-1105:Audiobooks
while(docCursor!!.moveToNext()) {
str = docCursor.getString(0)
if(str.matches(Regex(".*:.*"))) break //Maybe useless
}
docCursor.close()
val split = str.split(":")
val base: File =
if(split[0] == "primary") getExternalStorageDirectory()
else File("/storage/${split[0]}")
if(!base.isDirectory) throw Exception("'$uri' cannot be resolved in a valid path")
return File(base,split[1]).canonicalPath
}
I am working on an application where app writes a log file with the currentdate as filename
Ex: 20200710.txt
The earlier was working fine before android 10 but from android 10 the code is no longer writing the file in the external storage.
So I have modified the code a bit for android 10 especially
string logDir = "Documents/MyApp_Data/logs/";
Context context = MyApplication.Context;
ContentValues values = new ContentValues();
values.Put(MediaStore.MediaColumns.DisplayName, filename);
values.Put(MediaStore.MediaColumns.MimeType, "text/plain"); //file extension, will automatically add to file
values.Put(MediaStore.MediaColumns.RelativePath, logDir);
var uri = context.ContentResolver.Insert(MediaStore.Files.GetContentUri("external"), values);
Stream outputStream = context.ContentResolver.OpenOutputStream(uri, "rw");
outputStream.Write(Encoding.UTF8.GetBytes(message));
outputStream.Close();
The above code is working for android 10 but it is creating multiple log files instead I want to update the file if the file already exists. I am not getting a way to check if the file exists then append new data in the existing file. Can someone please let me know? The above code is in Xamarin android but if you have any suggestion that will work in android then I will convert that code to Xamarin android
Thanks in advance
This code corrects (especially words' upper/lower cases) vaibhav ones and use blackapps suggestion to include text append. Can write txt or json.
Good to write text in persistent folders (e.g. /storage/self/Downloads) without user interaction on Android 10+ (actually not tested on 11, but should work).
// filename can be a String for a new file, or an Uri to append it
fun saveTextQ(ctx: Context,
relpathOrUri: Any,
text: String,
dir: String = Environment.DIRECTORY_DOWNLOADS):Uri?{
val fileUri = when (relpathOrUri) {
is String -> {
// create new file
val mime = if (relpathOrUri.endsWith("json")) "application/json"
else "text/plain"
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, relpathOrUri)
values.put(MediaStore.MediaColumns.MIME_TYPE, mime) //file extension, will automatically add to file
values.put(MediaStore.MediaColumns.RELATIVE_PATH, dir)
ctx.contentResolver.insert(MediaStore.Files.getContentUri("external"), values) ?: return null
}
is Uri -> relpathOrUri // use given Uri to append existing file
else -> return null
}
val outputStream = ctx.contentResolver.openOutputStream(fileUri, "wa") ?: return null
outputStream.write(text.toByteArray(charset("UTF-8")))
outputStream.close()
return fileUri // return Uri to then allow append
}
I am using Android 7.0 mobile device for my testing.
I am implementing an android app.
I need to take a photo from device camera and just select it from camera folder/ Gallery using by the application.
But after taking a photo and go back to my application and try to select that captured photo by my app, when moving to camera folder, the recently taken photo is not in the gallery.
But I noticed when I take another photo and go to select that photo from again via my app, from the camera folder, I can see the photo I took previously but not the recently taken.
I followed this answer - Android: Refreshing the Gallery after saving new images. But it does not work for me.
If somebody can reply with the answer and that answer contains with a file path to find, please add those details also. ex:- "how to take android camera image gallery path"
The code I use here:-
val cursor: Cursor? = contentResolver.query(uri, projectionColumns, null, null, sortOrder)
if (cursor != null && cursor.moveToFirst()) {
while (cursor.moveToNext()) {
// Get values per item
val imageId = cursor.getString(cursor.getColumnIndex(projectionColumns[0]))
val imageName = cursor.getString(cursor.getColumnIndex(projectionColumns[1]))
val imagePath = cursor.getString(cursor.getColumnIndex(projectionColumns[2]))
val dateTaken = cursor.getString(cursor.getColumnIndex(projectionColumns[3]))
val imageSize = cursor.getString(cursor.getColumnIndex(projectionColumns[4]))
val bucketId = cursor.getString(cursor.getColumnIndex(projectionColumns[5]))
val bucketName = cursor.getString(cursor.getColumnIndex(projectionColumns[6]))
please note that if you are giving the intent a Uri or location somewhere in external or internal private directory to store the image picture after it gets clicked then you need to broadcast it manually or try to move it to another folder.
here is code to broadcast it by passing the path of the file.
public void broadCastMedia(Context context, String path) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
File file = new File(path);
intent.setData(Uri.fromFile(file));
context.sendBroadcast(intent);
}
and here is how to copy one to another location say pictures directory then you need to create a temp file to pictures directory and an original file and pass to this.
//temp file
File destFile = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "image.jpg");
//original file
File sourceFile = new File(uri.getPath());
public static Boolean copyFile(File sourceFile, File destFile)
throws IOException {
if (!destFile.exists()) {
destFile.createNewFile();
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(sourceFile).getChannel();
destination = new FileOutputStream(destFile).getChannel();
destination.transferFrom(source, 0, source.size());
} finally {
if (source != null)
source.close();
if (destination != null)
destination.close();
}
return true;
}
return false;
}
and if you have given EXTRA_OUTPUT in intent then you need to store the Uri you have passed as extra as data intent in onActivityResult() will be null at that time you can use the stored Uri to perform any operation on the stored file.