kotlin: How to read a Uri in Android? - android

I've been stuck with this for almost a week. Triend almost any answer/tutorial I found, but no luck yet.
I have an app that uses the camera. After the user takes a pic, and select the check/OK button in the camera, the camera returns, among other things, the Uri where the image was stored. It cames in the form of: content://media/external/images/media/122 (this is an actual pic in the phone), if I ask for the Uri.path I'll get in this case: /external/images/media/123
Now, with the Uri object, I can assign it to an imageView like imageView.setImageURI() and it works, so I'm pretty confident the images are stored somewhere.
Now, next thing my app should do, is take all the pictures taken, and send them to an API.
My problem is, when I try to read the Uri with any method, it gives me a FileNotFound Exception, no matter if I use the Uri or the path.
So I really don't know what to do. What am I missing? Do I have to explicitly save the image in a working directory? If so, how is it done? If not, how do I get the full path name including the filename? When I look at the DCIM directoy in the phone, I don't find the pics.
Thanks in advance!
EDIT
Code to take the pic:
btnTomarFoto.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(android.Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_DENIED || ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_DENIED
) {
// El permiso no fue concedido
val permission = arrayOf(android.Manifest.permission.CAMERA,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermissions(permission, PERMISSION_CODE)
} else {
// El permiso ya estaba concedido
openCamera()
}
} else {
// El SO es menor que Marshmallow
}
}
}
Code to open the camera:
private fun openCamera() {
val values = ContentValues()
values.put(MediaStore.Images.Media.TITLE, "New Picture")
values.put(MediaStore.Images.Media.DESCRIPTION, "From the camera")
image_uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, image_uri)
startActivityForResult(cameraIntent, IMAGE_CAPTURE_CODE)
}
Code to store the Uri into the database:
private fun guardarFoto() {
var foto = FotografiaModel(actividad_id!!, image_uri.toString(), txtObservacion.text.toString())
Log.w(tag, "URI: $image_uri PATH: " + image_uri!!.path + " Encoded Path: " + image_uri!!.encodedPath)
fotografiaDBHelper.insertFoto(foto)
visitaDBHelper.actualizaEstado(actividad_id!!, "INICIADO")
}
There are some more things the code does, but this is the main thing. To show the received Uri into an Imageview I use:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
imageView.setImageURI(image_uri)
Log.v(tag, "Image URI: $image_uri")
}
}

My problem is, when I try to read the Uri with any method, it gives me a FileNotFound Exception, no matter if I use the Uri or the path.
That is because it is not a file. It is a Uri.
For example, https://stackoverflow.com/questions/56839478/kotlin-how-to-read-a-uri-in-android is a Uri. Your code assumes that the path /questions/56839478/kotlin-how-to-read-a-uri-in-android is the only thing that matters in a Uri. By your argument, every single computer on the planet (plus those in orbit) have a file located at /questions/56839478/kotlin-how-to-read-a-uri-in-android. That is not how a Uri works. You need to use the entire Uri to determine how to use it. In this case, https as a scheme means that you use HTTPS as a protocol to talk to the designated server (stackoverflow.com) to retrieve the content.
Similarly, the content scheme in your Uri from insert() indicates that you use ContentResolver to work with it. In particular, you can use openInputStream() to get an InputStream on the content identified by the Uri, just as you might use HttpUrlConnection or OkHttp to get an InputStream on an https Uri.
In particular, since you delegated data storage to some other app (the MediaStore), where the image is stored is up to somebody else, and it is not necessarily on the filesystem in a place where you can access it. That is particularly true on Android Q and higher, where you have limited access to arbitrary locations on the device via filesystem APIs.
Now, next thing my app should do, is take all the pictures taken, and send them to an API.
I am going to guess that "send them to an API" means "upload them to a server" (versus "call an API exposed by a library on the device" or "call a method in the Android SDK"). In that case, you have some code for talking to that server. It might be general-purpose code (e.g., OkHttp, Retrofit) or API-specific code (e.g., Facebook SDK).
Regardless, you will need to see what that code supports for your image content:
If it supports a Uri, try using your Uri
If it supports InputStream (or a Reader of some type), use openInputStream() on a ContentResolver
If it supports a FileDescriptor or AssetFileDescriptor, use openFileDescriptor() on a ContentResolver
If it only supports File, instead of using contentResolver.insert() to get the Uri to send to the camera app, use FileProvider, so you can have the images saved in a file that you control (e.g., in getCacheDir())
Etc.

Related

creating file from uri in android 11 - getPath() not working

I want to create a file from uri in the ActivityResultCallback in android 11. I use uri.getPath() to convert the incoming uri to a file. but, it won't work in android 11. here is my codes:
private void launchGallery (){
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
launcherGallery.launch(intent);
}
ActivityResultLauncher<Intent> launcherGallery = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
Intent data = result.getData();
if (data != null) {
Uri imageUri = data.getData();
// ---> getPath() won't work in android 11 <---
File file = new File(imageUri.getPath());
// I don't want to display the image in an ImageView.
// I need a file object to pass it to this method to encrypt it
encryptImage(file);
}
}
});
so, how can I create a file from uri in android 11?
simply: you can't. if Uri would be always a File then there would be no purpose of both class existance. Uri is a "pointer" to a file, which can be read as InputStream
InputStream inputStream = getContentResolver().openInputStream(uri);
you have to read all these bytes and if you REALLY need a File then store this data in your apps Scoped Storage. if you are shure that this is an image (its declared in calling Intent) then you may try to convert this data to Bitmap.
edit: I see you've added encryptImage(..) method call, so for shure you can read Bitmap "straight from" Uri instead of a File without taking users storage space, even for a while
check out such case: note that when you run a file picker launchGallery() then inside of it you can switch folders. there is a possibility to pick file from e.g. Google Drive, or if you have installed, other web-storages (OneDrive, Dropbox etc.). you will get an Uri as a result, but this isn't a real File on local storage. so you can stream it (as through web/network) for reading entirelly, it isn't ready and available at the moment of result callback call (just after picking and returning to your app)
btw. such possibility isn't limited to Android 11 and above, it was there way earlier. just handle Uri as "pointer", do not assume it is pointing on (local) File. in some cases even if user pick local file you will get an Uri pointing on it through some ContentResolver (so use own for read data), thus this won't be a file path (won't be starting with file://)

How do I handle files on Android device that are created by a PC? API level 29

I would like to do the following:
Read and edit a file that is created on the device via USB cable connected to a PC.
Write files that are visible when the user opens the device's internal storage in the windows file explorer.
I target Android 10 (API level 29).
The best solution I was able to find for reading files was the deprecated
val containerFile = File(
getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"myFile.csv"
)
// Then I was able to read files from
val inputStream: InputStream = contentResolver.openInputStream(Uri.fromFile(containerFile))!!
This way when I placed "myFile.csv" in the downloads folder, my app was able to read the contents.
As for creating PC-readable files, the only solution I found was to create hidden temporary files, and whenever I had to make them readable from PC, I created an intent as follows:
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "file/csv"
putExtra(Intent.EXTRA_TITLE, "output.csv")
// I specify a URI for the directory that should be opened in the system file picker
putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
)
}
Is there any better solution?
If not, how do I get a path to a directory which is visible from PC, using API 29?
I know this should be possible, since there are many text editor apps for android, which are doing the same things I want to, but I was not able to find any tutorial.
I need a long term solution, I'm very confused...
Thank you very much!
Big thanks to #CommonsWare for the suggestions!
ACTION_OPEN_DOCUMENT_TREE and GET_PERSISTABLE_PERMISSION gives acces to a user selected folder and all the files in it. Anything created by the app is visible from PC.
What I needed was
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), GET_PERSISTABLE_PERMISSION)
and
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == GET_PERSISTABLE_PERMISSION && resultCode == Activity.RESULT_OK) {
//FileHandler.onActivityResult(data)
data?.data?.also{ uri->
val contentResolver = applicationContext.contentResolver
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
// Write a test file in the selected folder
// val pickedDir = DocumentFile.fromTreeUri(this, uri)
// val tmpFile = pickedDir.createFile("text/csv", "debugTestFile")
// val out: OutputStream? = getContentResolver().openOutputStream(tmpFile!!.uri)
// out?.write(("\uFEFF" + "árvíztűrő tükörfúrógép\r\n").toByteArray()) // adding BOM to the start for Excel to recognize utf8
// out?.close()
}
}
super.onActivityResult(requestCode, resultCode, data)
}

How to get information of an APK file in the file system (not just installed ones) without using File or file-path?

Background
My app (here) can search for APK files throughout the file system (not just of installed apps), showing information about each, allowing to delete, share, install...
As part of the scoped-storage feature on Android Q, Google announced that SAF (storage access framework) will replace the normal storage permissions. This means that even if you will try to use storage permissions, it will only grant to access to specific types of files for File and file-path to be used or completely be sandboxed (written about here).
This means that a lot of frameworks will need to rely on SAF instead of File and file-path.
The problem
One of them is packageManager.getPackageArchiveInfo , which given a file path, returns PackageInfo , which I can get various information about:
name (on the current configuration) , AKA "label", using packageInfo.applicationInfo.loadLabel(packageManager) . This is based on the current configuration of the device (locale, etc...)
package name , using packageInfo.packageName
version code , using packageInfo.versionCode or packageInfo.longVersionCode .
version number , using packageInfo.versionName
app icon, using various ways, based on the current configuration (density etc... ) :
a. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)
b. if installed, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )
c. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)
There are a lot more that it returns you and a lot that are optional, but I think those are the basic details about APK files.
I hope Google will provide a good alternative for this (requested here and here ), because currently I can't find any good solution for it.
What I've tried
It's quite easy to use the Uri that I get from SAF and have an InputStream from it :
#TargetApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
packageInstaller = packageManager.packageInstaller
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/vnd.android.package-archive"
startActivityForResult(intent, 1)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
val uri = resultData.data
val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
if (!isDocumentUri)
return
val documentFile = DocumentFile.fromSingleUri(this, uri)
val inputStream = contentResolver.openInputStream(uri)
//TODO do something with what you got above, to parse it as APK file
But now you are stuck because all the framework I've seen needs a File or file-path.
I've tried to find any kind of alternative using the Android framework but I couldn't find any. Not only that, but all libraries I've found don't offer such a thing either.
EDIT: found out that one of the libraries I've looked at (here) - kinda has the option to parse APK file (including its resources) using just a stream, but :
The original code uses a file path (class is ApkFile), and it takes about x10 times more than normal parsing using the Android framework. The reason is probably that it parses everything possible, or close to it. Another way (class is ByteArrayApkFile ) to parse is by using a byte-array that includes the entire APK content. Very wasteful to read the entire file if you need just a small part of it. Plus it might take a lot of memory this way, and as I've tested, indeed it can reach OOM because I have to put the entire APK content into a byte array.
I've found out it sometimes fails to parse APK files that the framework can parse fine (here). Maybe it will soon be fixed.
I tried to extract just the basic parsing of the APK file, and it worked, but it's even worse in terms of speed (here). Took the code from one of the classes (called AbstractApkFile). So out of the file, I get just the manifest file which shouldn't take much memory, and I parse it alone using the library. Here:
AsyncTask.execute {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val apkFilePath = packageInfo.applicationInfo.publicSourceDir
// I'm using the path only because it's easier this way, but in reality I will have a Uri or inputStream as the input, which is why I use FileInputStream to mimic it.
val zipInputStream = ZipInputStream(FileInputStream(apkFilePath))
while (true) {
val zipEntry = zipInputStream.nextEntry ?: break
if (zipEntry.name.contains("AndroidManifest.xml")) {
Log.d("AppLog", "zipEntry:$zipEntry ${zipEntry.size}")
val bytes = zipInputStream.readBytes()
val xmlTranslator = XmlTranslator()
val resourceTable = ResourceTable()
val locale = Locale.getDefault()
val apkTranslator = ApkMetaTranslator(resourceTable, locale)
val xmlStreamer = CompositeXmlStreamer(xmlTranslator, apkTranslator)
val buffer = ByteBuffer.wrap(bytes)
val binaryXmlParser = BinaryXmlParser(buffer, resourceTable)
binaryXmlParser.locale = locale
binaryXmlParser.xmlStreamer = xmlStreamer
binaryXmlParser.parse()
val apkMeta = apkTranslator.getApkMeta();
Log.d("AppLog", "apkMeta:$apkMeta")
break
}
}
}
So, for now, this is not a good solution, because of how slow it is, and because getting the app name and icon requires me to give the entire APK data, which could lead to OOM. That's unless maybe there is a way to optimize the library's code...
The questions
How can I get an APK information (at least the things I've mentioned in the list) out of an InputStream of an APK file?
If there is no alternative on the normal framework, where can I find such a thing that will allow it? Is there any popular library that offers it for Android?
Note: Of course I could copy the InputStream to a file and then use it, but this is very inefficient as I will have to do it for every file that I find, and I waste space and time in doing so because the files already exist.
EDIT: after finding the workaround (here) to get very basic information about the APK via getPackageArchiveInfo (on "/proc/self/fd/" + fileDescriptor.fd) , I still can't find any way to get app-label and app-icon. Please, if anyone knows how to get those with SAW alone (no storage permission), let me know.
I've set a new bounty about this, hoping someone will find some workaround for this as well.
I'm putting a new bounty because of a new discovery I've found: An app called "Solid Explorer" targets API 29, and yet using SAF it can still show APK information, including app name and icon.
That's even though in the beginning when it first targeted API 29, it didn't show any information about APK files, including the icon and the app name.
Trying out an app called "Addons detector", I couldn't find any special library that this app uses for this purpose, which means it might be possible to do it using the normal framework, without very special tricks.
EDIT: about "Solid Explorer", seems that they just use the special flag of "requestLegacyExternalStorage", so they don't use SAF alone, but the normal framework instead.
So please, if anyone knows how to get app-name and app-icon using SAF alone (and can show it in a working sample), please let me know.
Edit: seems that the APK-parser library can get the app name fine and the icons, but for icons it has a few issues:
the qualifiers are a bit wrong, and you need to find which is the best for your case.
For adaptive icon it can get a PNG instead of VectorDrawable.
For VectorDrawable, it gets just the byte-array. No idea how to convert it to a real VectorDrawable.
OK I think I found a way using the Android framework (someone on reddit gave me this solution), to use file-path and use it, but it's not perfect at all. Some notes:
Not as direct as before.
Good thing is that it might also be possible to handle even files that are outside of the device storage.
It looks like a workaround, and I'm not sure for how long it will work.
For some reason, I can't load the app label (it always returns just the package name instead), and same goes for the app-icon (always null or default icon).
The solution, in short, is using this:
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
I think this same approach can be used for all cases that you need a file-path.
Here's a sample app (also available here) :
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/vnd.android.package-archive"), 1
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
try {
val uri = data?.data ?: return
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
if (!isDocumentUri)
return
val documentFile = DocumentFile.fromSingleUri(this, uri) ?: return
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
Log.d("AppLog", "got APK info?${packageArchiveInfo != null}")
if (packageArchiveInfo != null) {
val appLabel = loadAppLabel(packageArchiveInfo.applicationInfo, packageManager)
Log.d("AppLog", "appLabel:$appLabel")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("AppLog", "failed to get app info: $e")
}
}
fun loadAppLabel(applicationInfo: ApplicationInfo, packageManager: PackageManager): String =
try {
applicationInfo.loadLabel(packageManager).toString()
} catch (e: java.lang.Exception) {
""
}
}
}
Use below code
/**
* Get the apk path of this application.
*
* #param context any context (e.g. an Activity or a Service)
* #return full apk file path, or null if an exception happened (it should not happen)
*/
public static String getApkName(Context context) {
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
String apk = ai.publicSourceDir;
return apk;
} catch (Throwable x) {
return null;
}
}

Managing local images with persistent data

I'm struggling to understand what could be a simple workaround for associating images and persistent data objects in android. In more details, i've put up a simple room persistence architecture and now I need to add a field "image" to the java persisted object. I've tried to work with uri but my knowledge of Android is very poor, and what I get is that the uri I recover when picking an image with the android file manager is only valid until reboot, so if I would save the so obtained uri in the database, it would make no sense when recovered later. How should I manage?
Basically what I need is a simple way to link an object to a local image stored in the phone (or captured on the fly with the camera), no worries about image deletion by the user or anything, just a simple way.
For istance I tried to tinker with the google code example but i clearly failed because I don't know what i'm doing
private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
this code results in compilation error, with required: parcedDescriptor... and VOID found, on the call of takePersistableUriPermission. I don't even know if that is a solution to my problem.
this is the code I use to get the uri from the local image, but I'm planning also to let the Camera snap a photo and pass it to for saving/linking it
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "*/*".
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
the uri I recover when picking an image with the android file manager is only valid until reboot, so if I would save the so obtained uri in the database, it would make no sense when recovered later
That's not quite accurate.
A Uri that you pull in via ACTION_OPEN_DOCUMENT will be good for whatever activity gets the Uri via onActivityResult(). If you pass that Uri to another component, you can use FLAG_GRANT_READ_URI_PERMISSION to allow that component to read the content at that Uri. But once your process ends, your access to that content goes away.
Since you used ACTION_OPEN_DOCUMENT, you can use takePersistableUriPermission() request to have long-term access to the content, but that still only works if the content is still there. If the user deletes the content, or perhaps even moves it, you will lose access.
For istance I tried to tinker with the google code example but i clearly failed because I don't know what i'm doing
takePersistableUriPermission() does not return a ParcelFileDescriptor. Otherwise, that particular call seems OK.
With respect to loading the image, please use an existing image-loading library (e.g., Glide, Picasso).

java.io.IOException: Cannot make changes to file

I am using JAudioTagger library for reading and writing tags for an audio file. I am able to read the tags but unable to write them.
I am retrieving audio file path like this :
private String getSongPath(long songId) {
String path = null;
ContentResolver contentResolver = getContentResolver();
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = {MediaStore.Audio.Media.DATA};
String selection = MediaStore.Audio.Media._ID + " == ?";
String[] selectionArgs = {String.valueOf(songId)};
Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, null);
if (cursor != null) {
int pathCol = cursor.getColumnIndexOrThrow(projection[0]);
cursor.moveToFirst();
path = cursor.getString(pathCol);
cursor.close();
}
return path;
}
Then to write tags using JAudioTagger :
File songFile = new File(path); // path looks like /storage/3932-3434/Music/xyz.mp3
AudioFile audiofile = = AudioFileIO.read(songFile);
Tag tag = = audiofile.getTag();
tag.setField(FieldKey.TITLE, title);
// some more setField calls for different feilds
audiofile.commit();
The commit() method is giving following Exception :
org.jaudiotagger.audio.exceptions.CannotWriteException:
java.io.IOException: Cannot make changes to file xyz.mp3 at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:799) at
com.techapps.musicplayerplus.MainActivity$17.onClick(MainActivity.java:2125)
at
android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157)
at android.os.Handler.dispatchMessage(Handler.java:102) at
android.os.Looper.loop(Looper.java:148) at
android.app.ActivityThread.main(ActivityThread.java:5417) 06-18
10:59:48.134 8802-8802/com.techapps.musicplayerplus W/System.err:
at java.lang.reflect.Method.invoke(Native Method) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused
by: java.io.IOException: Cannot make changes to file Saibo.mp3 at
org.jaudiotagger.audio.mp3.MP3File.precheck(MP3File.java:824) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:850) at
org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:783) at
org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:795)
I am running this code on Android 6 while my app is targeted at SDK 22. I have also mentioned following permission in manifest.
android.permission.WRITE_EXTERNAL_STORAGE
Still I am unable to write to SD card. Please help me. Thanks in advance.
You have to use Storage Access Framework (SAF) to access SD Card from API 19 (Kitkat) onward.
First we need to ask user to provide a URI of the folder we want to access. If we want access to entire SD card, user needs to provide URI of SD card's root folder.
For example, when user hits Edit button, we have to first show hint dialog box, asking user to select required directory in SD Card which we want to access. You can display following image in hint dialog box to ask user to select root directory of SD Card :
When user dismisses hint dialog box, you need to trigger Storage Access Framework :
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
pickedDir= DocumentFile.fromTreeUri(this, treeUri);
if (!isSDCardRootDirectoryUri(treeUri)) {
Toast.makeText(this, "Wrong directory selected. Please select SD Card root directory.", Toast.LENGTH_LONG).show();
createSDCardHintDialog().show();
return;
}
// Persist URI in shared preference so that you can use it later.
SharedPreferences sharedPreferences = getSharedPreferences(App.PREFERENCE_FILENAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(App.SDCARD_URI_KEY, treeUri.toString());
editor.apply();
// Persist access permissions, so you dont have to ask again
final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
private boolean isSDCardRootDirectoryUri(Uri treeUri) {
String uriString = treeUri.toString();
return uriString.endsWith("%3A");
}
Once you get Uri of user picked directory, you can perform write operation using SAF : (creadit : this answer )
public void writeFile(DocumentFile pickedDir) {
try {
DocumentFile file = pickedDir.createFile("image/jpeg", "try2.jpg");
OutputStream out = getContentResolver().openOutputStream(file.getUri());
try {
// write the image content
} finally {
out.close();
}
} catch (IOException e) {
throw new RuntimeException("Something went wrong : " + e.getMessage(), e);
}
}
It could be that you pointing to non existing file.
Check your path file by using Log.
Log.d("Activity", "path = " + path);
Android-M or API 23 introduced Runtime Permissions for reducing security flaws in android device.
To update your apps using Google Play services to handle Android 6.0 permissions, it’s good practice to manage the user’s expectations in setting permissions that the runtime may require. The following link will help you avoid potential issues.
https://developer.android.com/training/permissions/requesting.html
have you declared the permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ?
I saw that you already created an issue in the JAudioTagger GitHub repository which was advisable, but never got a universally working solution. My findings so far:
The answer mentioning SAF is correct, but it won't help you as SAF will provide a DocumentFile, not a File.
You might try to modify JAudioTagger to your needs, replacing File with DocumentFile, but the latter one has not all functions you will need.
Also InputStream and OutputStream will not help you, as JAudioTagger needs File and internally heavily uses RandomAccessFile which is not available either.
Google "forgot" to provide some getRandomAccessFileFromUri() which makes things even worse (Yes, there are hacks using Java reflection to work around this limitation...).
The "/proc/self/fd" method (How to handle SAF when I can only handle File or file-path?) will also not work immediately, as JAudioTagger needs copy and renaming functions that are not applicable to this kind of files. Particularly JAudioTagger will not find a suitable file name extension like ".m4a". Of course you could try to change JAudioTagger accordingly.
You might follow the advice to make a copy of the file to your personal storage, then apply JAudioTagger to it and finally copy it back to SD card, but:
If you want to use JAudioTagger to read from SD card, this will, as announced by Google, fail with Android 10. Starting with that version, you will not even have read access to the SD card via the File interface.
Further, the File interface gives you read access to SD cards with Android 9 and below, but not to other SAF devices, like USB OTG memory or SMB shares etc.
Of course you could also copy each file in order to read its metadata, but this will be awfully slow and is not suitable if you have more than a few files.
So my current advices are:
Try the "/proc/self/fd" method and modify JAudioTagger accordingly.
If the changes are too heavy, use the fd method for reading the tags and the copy method for writing.
BTW: I am currently modifying an older version of JAudioTagger for using both File and DocumentFile transparently, but the changes are tremendous, bear a high risk, need some help classes, and the work is unfinished, yet.
BTSW: The DocumentFile functions are painfully slow, compared to the File functions.

Categories

Resources