I see so many outdated approaches on how to download and share files in Java but couldn't seem to find any good approach on Kotlin.
There are just so many different ways and somehow most of them are not working properly.
I'm using MVVM + Clean Architecture and I simply want to download a file (mp3) from URL and share it with WhatsApp.
Here is what I found so far for Kotlin (just the download part):
override suspend fun downloadSound(soundURL: String) {
val storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
withContext(Dispatchers.IO) {
URL(soundURL).openStream().use { input ->
FileOutputStream(File(storage.absolutePath, "shareFile.mp3")).use { output ->
input.copyTo(output)
}
}
}
}
This used to work fine but out of a sudden I'm getting an error on Android 11+:
java.io.FileNotFoundException: /storage/emulated/0/Download/shareFile.mp3: open failed: EACCES (Permission denied)
On Android 10 I eliminated this problem with adding:
android:requestLegacyExternalStorage="true"
to my Manifest.
Here is how I share the sound after downloading:
try {
val storage =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(storage.absolutePath, "shareFile.mp3")
if (file.exists()) {
val uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
file
)
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.type = "audio/mp3"
context.startActivity(
Intent.createChooser(
intent,
"Share via"
)
)
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
Log.i("MeisterTag", "FILE DOES NOT EXIST")
}
I'm also not sure what permissions I need to request, if any.
For now I requested:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
What is the most up-to-date approach that is working for API 23+
Related
I am trying to copy a file from internal storage to external storage (SD card) in my Android application. I have the source file in a File object and the destination folder in a String, but I keep getting the error "java.io.FileNotFoundException: open failed: EACCES (Permission denied)".
I am trying to make a gallery, when I select the copy button, it launches an activity where an album is selected and the path is returned as follows as an extra:
var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// There are no request codes
val data: Intent? = result.data
val ruta: String = data?.getStringExtra("RUTA")!!
Toast.makeText(this, ruta, Toast.LENGTH_SHORT).show()
}
}
The album path is in this format: /storage/4329-1A0A/DCIM/Facebook
I have tried many different ways to copy the file, I have tried with MediaStore, Files.copy(), File.copy, shell commands, I have the following permissions in the manifest.xml and these permissions are requested at the start of the app:
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.MANAGE_MEDIA"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
This is the way I am trying to copy with shell:
fun copyFileWithShell(context: Context, src: File, dst: File) {
try {
val command = "cp ${src.absolutePath} ${dst.absolutePath}"
val process = Runtime.getRuntime().exec(command)
process.waitFor()
Toast.makeText(context, "File copied successfully", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(context, "Failed to copy file", Toast.LENGTH_SHORT).show()
}
}
This code gives me the error W/System.err: java.io.FileNotFoundException: open failed: EACCES (Permission denied)
fun copyOrMoveImage(operation: String, source: File, destination: String) {
try {
val fileInputStream = FileInputStream(source)
val fileOutputStream = FileOutputStream(destination + "/" + source.name)
val buf = ByteArray(1024)
var len: Int
while (fileInputStream.read(buf).also { len = it } > 0) {
fileOutputStream.write(buf, 0, len)
}
fileInputStream.close()
fileOutputStream.close()
if (operation == "MOVE") {
source.delete()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
val ruta: String = data?.getStringExtra("RUTA")!!
val file = File(media!![position].path)
when (OPERATION) {
"MOVE" -> copyOrMoveImage("MOVE", file, ruta)
"COPY" -> copyOrMoveImage("COPY", file, ruta)
}
This is the result of using geExternalFilesDirs():
/storage/emulated/0/Android/data/com.example.photo/files
/storage/emulated/0/
Also I declare android:requestLegacyExternalStorage="true" in the manifest
The targetSDK of the project is 33. The device where I am trying has Android Q.
Using Environment.getExternalStorageDirectory() returns the path of the Internal Storage ("/storage/emulated/0") instead of The SD Card path ("/storage/4329-1A0A/"), the same with getExternalFilesDirs()
I am using the code to share in an Mp4 video in WhatsApp
private fun shareVideoToApp(context: Context, packageName: String) {
sharePath?.let { path ->
val file = File(path)
startActivity(Intent().apply {
action = Intent.ACTION_SEND
setPackage(packageName)
putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, context.packageName + ".provider", file))
type = "video/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
})
} ?: run {
Log.e(TAG, "Path is null")
}
}
The share path variable has value
/storage/emulated/0/Android/data/com.sample.androidapp/files/Download/VideoSnippets/1632536010_0_9265.mp4
Sharing is pretty messed up after fileproviders and newer android versions
Try androidx sharecompat lib which is already included if u use androidx
new ShareCompat
.IntentBuilder(context)
.setType("video/*")
.setStream(uri)
.setText("Desired text to share")
.startChooser();
I am currently working with one of my app updates and I am looking for a way to save some files at the root of shared internal storage.
Don't confuse with the word shared here. I just meant here with the phone's internal/external storage which holds a large amount of data.
Now coming to the main point, I have an app that uses the FFmpeg library for android and it records the live streams and saves it into the phone's storage.
Now here's the problem I don't want to save this file in my app package folder.
e.g: /storage/emulated/0/Android/data/com.app.package/files/...
I want to save this video file in the root of the storage folder where I can create a special folder for my app just like WhatsApp then save all required data in it only.
e.g: /storage/emulated/0/MyApp/...
To be more concise I want my app just like WhatsApp which has a separate folder in internal storage for storing its app-related data.
Now, so far I tried these.
getExternalFilesDir(null)?.absolutePath -> /storage/emulated/0/Android/data/com.app.package/files
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath -> /storage/emulated/0/Android/data/com.app.package/files/Download
externalCacheDir?.absolutePath -> /storage/emulated/0/Android/data/com.app.package/cache
filesDir?.absolutePath -> /data/user/0/com.app.package/files
Environment.getRootDirectory().absolutePath -> /system
Environment.getExternalStorageDirectory().absolutePath -> /storage/emulated/0
Environment.getExternalStoragePublicDirectory("").absolutePath -> /storage/emulated/0
Now these to methods can work for me but they are deprecated getExternalStorageDirectory() & getExternalStoragePublicDirectory()
Just keep in mind my solution is related to the WhatsApp storage folder.
I searched a lot but not so much help as I don't want to go with just a copy-paste and hack solutions. I want to do it in a clean way.
I am trying to open a File Picker via intent for this I have set permission
....
....
and for intent.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.type = "*/*"
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
startActivityForResult(Intent.createChooser(intent, "Select .... file"), RC)
Now here on Android 10, a file picker is not opening and this works perfectly if I add the requestLegacyExternalStorage flag in the manifest.
Or
if I set android:maxSdkVersion="29" also worked but here google wants to use Some SAF or Media Store API but I don't get it I just want to pick a file just a simple txt nothing else.
You can save video file to external storage using this:
#RequiresApi(api = Build.VERSION_CODES.Q)
#Throws(FileNotFoundException::class)
fun addToApiAbove29Gallery(
context: Context,
file: File?,
fileName: String?,
destinationPath: String?,
action: () -> Unit
) {
val valuesvideos: ContentValues
valuesvideos = ContentValues()
valuesvideos.put(
MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM
+ File.separator + context.resources.getString(R.string.app_name)
+ File.separator + File(destinationPath).name
)
valuesvideos.put(MediaStore.Video.Media.TITLE, "${fileName}.mp4")
valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, "${fileName}.mp4")
valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1)
val resolver = context.contentResolver
val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uriSavedVideo = resolver.insert(collection, valuesvideos)
val pfd = context.contentResolver.openFileDescriptor(uriSavedVideo!!, "w")
val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
val handler = Handler(Looper.getMainLooper())
executor.execute {
if (pfd != null) {
try {
val out =
FileOutputStream(pfd.fileDescriptor)
val inputStream = FileInputStream(file)
val buf = ByteArray(8192)
var len: Int
while (inputStream.read(buf).also { len = it } > 0) {
out.write(buf, 0, len)
}
out.close()
inputStream.close()
pfd.close()
valuesvideos.clear()
valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 0)
try {
setExifFromUri(
context,
uriSavedVideo,
File(destinationPath)
)
} catch (e: IOException) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
handler.post {
context.contentResolver.update(uriSavedVideo, valuesvideos, null, null)
action()
}
}
}
}
Note that this code is only for android 10 and above, because you can easily save the video file to ecternal storage below android 10.
I am trying to save an image from the camera and it keeps returning false. I thought the image wasn't being displayed but when I looked at the filesystem I have lots of files but they are all of length zero.
Just some set up here.
val pictureResult = remember { mutableStateOf<Boolean?>(null)}
val context = LocalContext.current
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
pictureResult.value = it
// $it is false
}
This will create the file that is in the screenshot.
fun createImageFile(extension:String): File {
// Create an image file name
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val imagePath: File = File(context.filesDir, directoryFromExtension(extension))
return File(imagePath, "justme${timeStamp}${extension}"
).apply {
myNewsCreatorViewModel.currentPhotoPath = absolutePath
}
}
When the button is clicked I go through these steps, starting with the file returned from the previous method.
val uri = FileProvider.getUriForFile(context, "com.mine.fileprovider", file)
cameraLauncher.launch(uri)
This is the result after I set the pictureResult.value, the idea being to then show the image, or I currently show a text message if imageSaved was false that the file wasn't saved.
pictureResult.value?.let { imageSaved ->
run {
// imageSaved is false
}
}
I can't tell what I am doing wrong, and I can't tell how to get more information as to why it failed to save the image properly.
In my case the issue was caused by the android:launchMode="singleInstance" setting in the app manifest. The contract returned false, but I could load the image from the given URI. After removing the setting, it started working.
It appears the solution is to add:
val storageDir: File? = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), directoryFromExtension(extension))
if(storageDir?.exists() == false && ".jpg" == extension) {
storageDir.mkdirs()
}
And I am not certain if it helps but it works if I also put this before I call the intent.
context.grantUriPermission("com.mine.android", uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
Do you have the following permissions declared in your AndroidManifest.xml?
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
IMO it's always a good idea to revisit Take photos from the official docs if you're bumping into unknown issues.
Anyone else finding scoped-storage near-impossible to get working? lol.
I've been trying to understand how to allow the user to give my app write permissions to a text file outside of the app's folder. (Let's say allow a user to edit the text of a file in their Documents folder). I have the MANAGE_EXTERNAL_STORAGE permission all set up and can confirm that the app has the permission. But still every time I try
val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "rwt")?.fileDescriptor
I get the Illegal Argument: Media is read-only error.
My manifest requests these three permissions:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
I've also tried using legacy storage:
<application
android:allowBackup="true"
android:requestLegacyExternalStorage="true"
But still running into this read-only issue.
What am I missing?
extra clarification
How I'm getting the URI:
view?.selectFileButton?.setOnClickListener {
val intent =
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)
}
and then
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 111 && resultCode == AppCompatActivity.RESULT_OK && data != null) {
val selectedFileUri = data.data;
if (selectedFileUri != null) {
viewModel.saveFilename(selectedFileUri.toString())
val contentResolver = context!!.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(selectedFileUri, takeFlags)
view?.fileName?.text = viewModel.filename
//TODO("if we didn't get the permissions we needed, ask for permission or have the user select a different file")
}
}
}
You may try the code below. It works for me.
class MainActivity : AppCompatActivity() {
private lateinit var theTextOfFile: TextView
private lateinit var inputText: EditText
private lateinit var saveBtn: Button
private lateinit var readBtn: Button
private lateinit var deleteBtn: Button
private lateinit var someText: String
private val filename = "theFile.txt"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (!isPermissionGranted()) {
val permissions = arrayOf(WRITE_EXTERNAL_STORAGE)
for (i in permissions.indices) {
requestPermission(permissions[i], i)
}
}
theTextOfFile = findViewById(R.id.theTextOfFile)
inputText = findViewById(R.id.inputText)
saveBtn = findViewById(R.id.saveBtn)
readBtn = findViewById(R.id.readBtn)
deleteBtn = findViewById(R.id.deleteBtn)
saveBtn.setOnClickListener { savingFunction() }
deleteBtn.setOnClickListener { deleteFunction() }
readBtn.setOnClickListener {
theTextOfFile.text = readFile()
}
}
private fun readFile() : String{
val rootPath = "/storage/emulated/0/Download/"
val myFile = File(rootPath, filename)
return if (myFile.exists()) {
FileInputStream(myFile).bufferedReader().use { it.readText() }
}
else "no file"
}
private fun deleteFunction(){
val rootPath = "/storage/emulated/0/Download/"
val myFile = File(rootPath, filename)
if (myFile.exists()) {
myFile.delete()
}
}
private fun savingFunction(){
deleteFunction()
someText = inputText.text.toString()
val resolver = applicationContext.contentResolver
val values = ContentValues()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
val uri = resolver.insert(MediaStore.Files.getContentUri("external"), values)
uri?.let { it ->
resolver.openOutputStream(it).use {
// Write file
it?.write(someText.toByteArray(Charset.defaultCharset()))
it?.close()
}
}
} else {
val rootPath = "/storage/emulated/0/Download/"
val myFile = File(rootPath, filename)
val outputStream: FileOutputStream
try {
if (myFile.createNewFile()) {
outputStream = FileOutputStream(myFile, true)
outputStream.write(someText.toByteArray())
outputStream.flush()
outputStream.close()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun isPermissionGranted(): Boolean {
val permissionCheck = ActivityCompat.checkSelfPermission(this, WRITE_EXTERNAL_STORAGE)
return permissionCheck == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission(permission: String, requestCode: Int) {
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
}
}
In terms of your code:
None of your listed permissions have anything to do with ACTION_OPEN_DOCUMENT
Neither of the flags on your Intent belong there
Your real problem, though, is that you appear to be choosing media, such as from the Audio category. ACTION_OPEN_DOCUMENT guarantees that we can read from the content identified by the Uri, but it does not guarantee a writeable location. Unfortunately, MediaProvider blocks all write access, throwing the exception whose message you cited.
Quoting myself from the issue that I filed last year:
The problem is that we have no way of specifying on the ACTION_OPEN_DOCUMENT Intent that we intend to write and therefore want to limit the user to writable locations. Given that Android Q/R are putting extra emphasis on us migrating to the Storage Access Framework, this sort of feature is needed. Otherwise, all we can do is detect that we do not have write access (e.g., DocumentFile and canWrite()), then tell the user "sorry, I can't write there", which leads to a bad user experience.
I wrote a bit more about this problem in this blog post.
So, use DocumentFile and canWrite() to see if you are allowed to write to the location identified by the Uri, and ask the user to choose a different document.
On Android 11 and testing with API 30 emulators i found public folders like
Download, Documents, DCIM, Alarms, Pictures and such
writable for my apps using classic file system paths.
Restricted to app's own files.
Further i found that files created by one app in this way were writeble by a different app using SAF.