I am working on a music player and want to use Storage access framework to access the files in the storage. For that I used Intent.ACTION_OPEN_DOCUMENT_TREE and contentResolver.takePersistableUriPermission(...). But once I've got the permissions, I have to store the allowed path so I am using SharedPreferences for that.
When I convert the URI I got from the Intent.ACTION_OPEN_DOCUMENT_TREE to string to reuse it, it gives me a nullPointerException(It says the URI I got from converting the string from preferences is null).
While if I use the same URI without going through saving to the SharedPreferences, It works just fine.
Here's the code:
package com.gaurav712.music
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity() {
private val openDocumentTreeRequestCode: Int = 40
private lateinit var setPreferences: SharedPreferences
private lateinit var rootUri: Uri
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setPreferences = getPreferences(Context.MODE_PRIVATE)
// Check if access to storage is permitted
checkAccess()
// Now that we have access to storage, set the root Uri
rootUri = Uri.parse(setPreferences.getString("allowed_path", ""))
Log.i("rootUri", rootUri.toString())
// listFiles()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == openDocumentTreeRequestCode && resultCode == RESULT_OK) {
val treeUri = data?.data // get data
if (treeUri != null) {
contentResolver.takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
val editor = setPreferences.edit()
editor.putBoolean("got_permissions", true)
editor.putString("allowed_path", treeUri?.toString())
editor.apply()
Log.i("treeUri", treeUri.toString())
val documentFile = treeUri?.let { DocumentFile.fromTreeUri(this, it) }
for (file in documentFile?.listFiles()!!) {
file.name?.let { Log.i("file: ", it) }
}
}
}
private fun checkAccess() {
val gotPermission: Boolean = setPreferences.getBoolean("got_permissions", false)
if (!gotPermission)
getAccess()
else
Log.i("got_permissions",
gotPermission.toString()
+ " : "
+ setPreferences.getString("allowed_path", ""))
}
private fun getAccess() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, openDocumentTreeRequestCode)
}
private fun listFiles() {
val documentFile: DocumentFile = DocumentFile.fromTreeUri(this, rootUri)!!
for (file in documentFile.listFiles()) {
Log.i("file: ", file.name.toString())
}
}
}
In onCreate() if I uncomment listFiles(), It gives nullPointerException but using the same chunk of code as you can see above in onActivityResult(), it all works fine. The chunk I am talking about:
val documentFile = treeUri?.let { DocumentFile.fromTreeUri(this, it) }
for (file in documentFile?.listFiles()!!) {
file.name?.let { Log.i("file: ", it) }
}
I can't figure out why it says rootUri is null.
I looked at all the similar questions asked like this one. I am using the same functions suggested (toString() and Uri.parse) for conversion but it doesn't seem to work with Storage access framework.
I solved it! the function getAccess() should be:
private fun getAccess() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
startActivityForResult(intent, openDocumentTreeRequestCode)
}
I was missing Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION flag in the intent.
Related
I want to save images taken from my app directly to a ssd drive (removable storage) plugged in my device.
The issue I have now, is that with Android 11, I didn't manage to get the path of this storage, and so I can't write the files...
I tried use Storage Access Framework to ask the user to specify the path directly for each images but I can't use this solution as I need to write 30 images per seconds and it kept asking the user select an action on the screen.
This application is only for internal use, so I can grant all the permission without any Google deployment politics issues.
Can anybody help me, i'm so desperate...
So here's my code, I can write on a folder the user choose with SAF. Still have speed issue using DocumentFile.createFile function.
package com.example.ssdwriter
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.*
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity() {
private val TAG = "SSDActivity"
private val CONTENT = ByteArray(2 * 1024 * 1024)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
grantDirectoryAccess()
}
private fun grantDirectoryAccess() {
val treeUri = contentResolver.persistedUriPermissions
if (treeUri.size > 0) {
Log.e(TAG, treeUri.size.toString())
startWriting(treeUri[0].uri)
} else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
result.data?.data?.let {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
startWriting(result.data?.data!!)
}
}
resultLauncher.launch(intent)
}
}
private fun startWriting(uri: Uri) {
var handlerThread = HandlerThread("writer")
handlerThread.start()
var counter = 0
val handler = Handler(handlerThread.looper)
val runnableCode: Runnable = object : Runnable {
override fun run() {
Log.e(TAG, "Writing File $counter")
createFile(uri, counter++)
Log.e(TAG, "File $counter written ")
if(counter <= 150){
handler.postDelayed(this, 33)
}
}
}
handler.post(runnableCode)
}
private fun createFile(treeUri: Uri, counter: Int) {
val dir = DocumentFile.fromTreeUri(this, treeUri)
val file = dir!!.createFile("*/bmp", "Test$counter.bmp")
if (file != null) {
var outputStream = contentResolver.openOutputStream(file.uri)
if (outputStream != null) {
outputStream.write(CONTENT)
outputStream.flush()
outputStream.close()
}
}
}
}
If anyone got some clues to make this faster, it would be great !
I try to make an Export Function for my App, that can export(and later Import) data to a txt-file in the shared storage. I use the ACTION_OPEN_DOCUMENT_TREE to choose a Folder and get "resultdata: Intent?" back.
For explanation in the Downloads-folder/Documents-Folder there is a "myApp"-folder. The user gave permission for that folder. So i get an Intent? with the path to this place back
How can i use that to create a "Spells.txt" in said folder without ACTION_CREATE_DOCUMENT
Edit: Thanks to blackapps, I've found DocumentFile, which helped create and fill the File. Here are the relevant parts of my Code so far:
lateinit var permissedFolder: Intent
in onCreate
permissiontest_btn_Choose_Permission.setOnClickListener(){chooseFolder()}
permissiontest_btn_createFile.setOnClickListener(){createFile()}
fun chooseFolder(){
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
}
startActivityForResult(intent, 100)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if(requestCode==100 && resultCode== RESULT_OK){
if (resultData != null) {
permissedFolder = resultData
}
fun createFile() {
if(permissedFolder.data != null) {
val folderUri:Uri = permissedFolder.data!!
val enabledDirectory:DocumentFile= DocumentFile.fromTreeUri(this, folderUri)!!
val spellsExportDF =enabledDirectory.createFile(".txt","Spells.txt")
val spellsExportURI = spellsExportDF?.uri
if(spellsExportURI != null) {
val fileOutputStream = getContentResolver().openOutputStream(spellsExportURI)
//FileOutputStream(spellsExportURI, true)
fileOutputStream.use { fileout ->
fileout?.writer(Charsets.UTF_8)?.use {
it.write(test)
it.flush()
it.close()
}
}
val readfile = spellsExportDF.canRead()
val writefile =spellsExportDF.canWrite()
Toast.makeText(this, "can Read: $readfile", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "can Write: $writefile", Toast.LENGTH_SHORT).show()
}
I am making a specimen inventorying android app in Kotlin using Google Firebase. I use the firebase Realtime Datebase to store the specimen details & use firebase storage to store a picture of the specimen.
Eventually I will want to pull all of this data into the app to browse.
My Question: What is the best way to link the specimen's details in Realtime db to it's associated picture in Storage?
my code
RockEntry.kt
package com.inven.rock_stock
import android.util.Log
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
import java.util.*
class RockEntry {
var name = ""
var purchDate = ""
var local = ""
var mine = ""
var weight = ""
var paid = ""
var asking = ""
var description = ""
var dimensions = ""
var specimenNumber = ""
var Uid = ""
var database = FirebaseDatabase.getInstance()
var ref = database.getReference("Rocks")
constructor(name:String,purchDate:String,local:String,mine:String,
weight:String,dimensions:String,paid:String,asking:String,
description:String,Uid:String){
this.name = name
this.purchDate = purchDate.toString()
this.local = local
this.mine = mine
this.weight = weight
this.dimensions = dimensions
this.paid = paid
this.asking = asking
this.description = description
this.Uid = UUID.randomUUID().toString()
}
MainActivity.kt
package com.inven.rock_stock
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.tasks.Continuation
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageReference
import kotlinx.android.synthetic.main.activity_main.*
import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URI
var CAMERA_REQUEST_CODE = 0
var database = FirebaseDatabase.getInstance()
var ref = database.getReference("Rocks")
private var mStorageRef: StorageReference? = null
class MainActivity : AppCompatActivity() {
private val TAG = "MyActivity"
override fun onCreate(savedInstanceState: Bundle?) {
mStorageRef = FirebaseStorage.getInstance().getReference("ImagesBB")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
makeQuery()
}
imageBtn.setOnClickListener {
takePicture()
}
}
private fun makeQuery(){
var name = name.text.toString()
var purchDate = purchDate.toString()
var local = locality.text.toString()
var mine = mine.text.toString()
var weight = weight.text.toString()
var dimensions = dimensions.text.toString()
var paid = paid.text.toString()
var asking = asking.text.toString()
var description = description.text.toString()
if (!name.isBlank()) {
ref.child(name.toLowerCase()).setValue(
RockEntry(
name,
purchDate,
local,
mine,
weight,
paid,
asking,
dimensions,
description
)
)
}
else {
Toast.makeText(applicationContext, "Type in a name", Toast.LENGTH_LONG).show()
}
}
private fun takePicture() {
CAMERA_REQUEST_CODE = 222
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
try {
startActivityForResult(takePictureIntent, CAMERA_REQUEST_CODE)
} catch (e: ActivityNotFoundException) {
// display error state to the user
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
CAMERA_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK && data != null) {
val imageBitmap = data.extras?.get("data") as Bitmap
val baos = ByteArrayOutputStream()
imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val datar = baos.toByteArray()
mStorageRef!!.putBytes(datar)
}
}
}
}
}
'Linking' a FireBase Storage file to your Realtime Database can be accomplished by either
getting the location where a file lives in FireBase storage, via
toString() on the storage reference
or
getting a URL where you can download the file, via the .downloadUrl function.
Either of these options can then be stored as text into a Realtime Database 'entry'.
code example:
yourStorageBase?.putFile(element)?.addOnSuccessListener {
yourStorageBase?.downloadUrl?.addOnSuccessListener { downloadUri ->
// #1
var filepath = yourStorageBase.toString()
// #2
var downloadLink = downloadUri.toString()
}
}
Note:
If the following code is used within a function, you may 'prematurely' return to the caller before the picture has finished uploading, meaning you can't use the 'links' immediately. You can write a coroutine to ensure the IO has time to complete.
User RecyclerView to load all data in your app using live model class with data adapter.
User DataSnapShot in firebase to get all of the child nodes from a parent.
In Firebase DB, You have specimen which holds a number of Specimen, then you need to have one picture node as a child of each specimen node. When you upload the picture to Firebase Storage fetch its URL and insert it to the picture node under that particular Specimen.
Its my first kotlin project and its been 3 months now since I started. I'm making a social app. Now I'm having difficulty in writing the code for recording and uploading video file on Firebase Storage and Database. Below is my code for the Video Post Creation Activity which handles the uploading and publishing. I tried many modifications but the app crashes on this code now whenever I tap on creating a new post or uploading a video. Can you please check what's wrong here. As my intent is only recording a video, and writing food name, place etc and then publishing the post. I'm actually confused on my code.
import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.text.TextUtils
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.tasks.Continuation
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageReference
import com.google.firebase.storage.StorageTask
import com.google.firebase.storage.UploadTask
import kotlinx.android.synthetic.main.activity_add_post.*
class AddPostActivity : AppCompatActivity() {
private var myUrl = ""
private var videoUri: Uri? = null
private var storagePostPicRef: StorageReference? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_post)
storagePostPicRef = FirebaseStorage.getInstance().reference.child("Posts Videos")
save_new_post_btn.setOnClickListener { uploadImage() }
intent.action = MediaStore.ACTION_VIDEO_CAPTURE
startActivityForResult(intent, 101)
// var intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
// startActivityForResult(intent, VIDEO_CAPTURE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 101 && resultCode == Activity.RESULT_OK && data != null) {
val result = videoUri
video_new_post.setVideoURI(videoUri)
}
}
private fun getPath(uri: Uri): String {
var projectionArray = arrayOf(MediaStore.Video.Media.DATA)
var cursor: Cursor? =
applicationContext.contentResolver.query(uri, projectionArray, null, null, null)
if (cursor != null) {
val columnIndex: Int = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
cursor.moveToFirst()
return cursor.getString(columnIndex)
} else {
return ""
}
}
private fun uploadImage() {
when {
videoUri == null -> Toast.makeText(this, "Please select a Video!", Toast.LENGTH_LONG)
.show()
TextUtils.isEmpty(name_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(place_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(city_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(state_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
else -> {
val progressDialog = ProgressDialog(this)
progressDialog.setTitle("Upload Started...")
progressDialog.setMessage("Great! Please wait until we bake it...")
progressDialog.show()
val fileRef =
storagePostPicRef!!.child(System.currentTimeMillis().toString() + ".mp4")
var uploadTask: StorageTask<*>
uploadTask = fileRef.putFile(videoUri!!)
uploadTask.continueWithTask(Continuation<UploadTask.TaskSnapshot, Task<Uri>> { task ->
if (!task.isSuccessful) {
task.exception?.let {
throw it
progressDialog.dismiss()
}
}
return#Continuation fileRef.downloadUrl
})
.addOnCompleteListener(OnCompleteListener<Uri> { task ->
if (task.isSuccessful) {
val downloadUrl = task.result
myUrl = downloadUrl.toString()
val ref = FirebaseDatabase.getInstance().reference.child("Posts")
val postId = ref.push().key
val postMap = HashMap<String, Any>()
postMap["postid"] = postId!!
postMap["foodname"] = name_new_post.text.toString().toLowerCase()
postMap["placename"] = place_new_post.text.toString().toLowerCase()
postMap["cityname"] = city_new_post.text.toString().toLowerCase()
postMap["statename"] = state_new_post.text.toString().toLowerCase()
postMap["publisher"] = FirebaseAuth.getInstance().currentUser!!.uid
postMap["postimage"] = myUrl
ref.child(postId).updateChildren(postMap)
Toast.makeText(this, "Upload Finished Successfully!", Toast.LENGTH_LONG)
.show()
val intent = Intent(this#AddPostActivity, MainActivity::class.java)
startActivity(intent)
finish()
progressDialog.dismiss()
} else {
progressDialog.dismiss()
}
})
}
}
}
}
So after adjusting my code and restructuring it, this is what I came up with, and its still not opening anything, just closes the app when tried to create a post. It should open up the camera for recording though.
import android.app.ProgressDialog
import android.content.Intent
import android.database.Cursor
import android.icu.text.SimpleDateFormat
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.google.android.gms.tasks.Continuation
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageReference
import com.google.firebase.storage.StorageTask
import com.google.firebase.storage.UploadTask
import kotlinx.android.synthetic.main.activity_add_post.*
import java.io.File
import java.io.IOException
class AddPostActivity : AppCompatActivity() {
private var myUrl = ""
private var videoUri: Uri? = null
private var storagePostPicRef: StorageReference? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_post)
storagePostPicRef = FirebaseStorage.getInstance().reference.child("Posts Videos")
save_new_post_btn.setOnClickListener { uploadImage() }
dispatchTakeVideoIntent()
}
fun createVideoFile(): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(java.util.Date())
val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_MOVIES)
return File.createTempFile(
"Video_${timeStamp}_", /* prefix */
".mp4", /* suffix */
storageDir /* directory */
)
}
val REQUEST_VIDEO_CAPTURE = 101
private fun dispatchTakeVideoIntent() {
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
takeVideoIntent.resolveActivity(packageManager)?.also {
val videoFile: File? = try {
createVideoFile()
} catch (ex: IOException) {
null
}
// Continue only if the File was successfully created
videoFile?.also {
videoUri = FileProvider.getUriForFile(
this,
"your_app_id.provider", // add your application id , copy from build.gradle file paste here
it
)
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri)
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE)
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 101 && resultCode == RESULT_OK) {
Log.d("VideoPath", videoUri?.path) // you can see video path in log
uploadImage()
}
}
private fun uploadImage() {
when {
videoUri == null -> Toast.makeText(this, "Please select a Video!", Toast.LENGTH_LONG)
.show()
TextUtils.isEmpty(name_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(place_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(city_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
TextUtils.isEmpty(state_new_post.text.toString()) -> Toast.makeText(
this,
"Please fill all the fields!",
Toast.LENGTH_LONG
).show()
else -> {
val progressDialog = ProgressDialog(this)
progressDialog.setTitle("Upload Started...")
progressDialog.setMessage("Great! Please wait until we bake it...")
progressDialog.show()
val fileRef =
storagePostPicRef!!.child(System.currentTimeMillis().toString() + ".mp4")
fileRef.putFile(videoUri!!).addOnSuccessListener {
fileRef.downloadUrl.addOnSuccessListener {
myUrl = it.toString()
val ref = FirebaseDatabase.getInstance().reference.child("Posts")
val postId = ref.push().key
val postMap = HashMap<String, Any>()
postMap["postid"] = postId!!
postMap["foodname"] = name_new_post.text.toString().toLowerCase()
postMap["placename"] = place_new_post.text.toString().toLowerCase()
postMap["cityname"] = city_new_post.text.toString().toLowerCase()
postMap["statename"] = state_new_post.text.toString().toLowerCase()
postMap["publisher"] = FirebaseAuth.getInstance().currentUser!!.uid
postMap["postimage"] = myUrl
ref.child(postId).setValue(postMap)
Toast.makeText(this, "Upload Finished Successfully!", Toast.LENGTH_LONG)
.show()
progressDialog.dismiss()
/* val intent = Intent(this#AddPostActivity, MainActivity::class.java)
startActivity(intent)
finish()*/
// if you want to return MainActivity just use finish()
finish()
}
}.addOnFailureListener {
progressDialog.dismiss()
}
}
}
}
}
We are using getUriForFile(Context, String, File) which returns a
content:// URI. For more recent apps targeting Android 7.0 (API level
24) and higher, passing a file:// URI across a package boundary causes
a FileUriExposedException. Therefore, we now present a more generic
way of storing images using a FileProvider.
We create path file like this: res/xml/file_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="external_files" path="." />
</paths>
Represents files in the root of your app's external storage area.
Add provider in manifest.xml
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths" />
</provider>
...
</application>
</manifest>
Here's a function that invokes an intent to capture video.
val REQUEST_VIDEO_CAPTURE = 101
private fun dispatchTakeVideoIntent() {
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
takeVideoIntent.resolveActivity(packageManager)?.also {
val videoFile: File? = try {
createVideoFile()
} catch (ex: IOException) {
null
}
// Continue only if the File was successfully created
videoFile?.also {
videoUri = FileProvider.getUriForFile(
this,
"${your_app_id}.provider",
it
)
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri)
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE)
}
}
}
}
Here's an example solution in a method that returns a unique file name for a new video using a date-time stamp:
#Throws(IOException::class)
private fun createVideoFile(): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(java.util.Date())
val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_MOVIES)
return File.createTempFile(
"Video_${timeStamp}_", /* prefix */
".mp4", /* suffix */
storageDir /* directory */
)
}
Continue if the process is successfully
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
Log.d("VideoPath",videoURI?.path) // you can see video path in log
uploadImage()
}
}
D/VideoPath:
/external_files/Movies/Video_20200510_143935_5711184798783973477.mp4
Finally, we make a change in the image upload function. We need to make this change to get the image download url:
if (task.isSuccessful) {
fileRef.downloadUrl().addOnSuccessListener {
myUrl = it.toString()
val ref = FirebaseDatabase.getInstance().reference.child("Posts")
val postId = ref.push().key
val postMap = HashMap<String, Any>()
postMap["postid"] = postId!!
postMap["foodname"] = name_new_post.text.toString().toLowerCase()
postMap["placename"] = place_new_post.text.toString().toLowerCase()
postMap["cityname"] = city_new_post.text.toString().toLowerCase()
postMap["statename"] = state_new_post.text.toString().toLowerCase()
postMap["publisher"] = FirebaseAuth.getInstance().currentUser!!.uid
postMap["postimage"] = myUrl
ref.child(postId).updateChildren(postMap)
Toast.makeText(this, "Upload Finished Successfully!", Toast.LENGTH_LONG)
.show()
progressDialog.dismiss()
/* val intent = Intent(this#AddPostActivity, MainActivity::class.java)
startActivity(intent)
finish()*/
// if you want to return MainActivity just use finish()
finish()
}
} else {
progressDialog.dismiss()
}
I'm using the emulator and I send the intent in this way:
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Intent i;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
i = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
} else {
return null;
}
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
and in onActivityResult():
Uri uri = data.getData();
if (uri == null)
return null;
context.getContentResolver()
.takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Every time I reboot, the app doesn't have access. Can anyone confirm about this bug on beta 6 on the emulator? Am I doing anything wrong?
I converted your code snippets into this Kotlin activity:
package com.commonsware.jetpack.myapplication
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.os.storage.StorageManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
private const val PREF_URI = "uri"
private const val REQUEST_SAF = 1337
class MainActivity : AppCompatActivity() {
private val prefs: SharedPreferences by lazy {
getSharedPreferences("test", Context.MODE_PRIVATE)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val uriString = prefs.getString(PREF_URI, null)
val storageManager = getSystemService(StorageManager::class.java)!!
if (uriString == null) {
startActivityForResult(
storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(),
REQUEST_SAF
)
} else {
val uri = Uri.parse(uriString)
val docFile = DocumentFile.fromTreeUri(this, uri)
Toast.makeText(
this,
"canRead: ${docFile?.canRead()} canWrite: ${docFile?.canWrite()}",
Toast.LENGTH_LONG
).show()
}
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
val uri = data?.data
if (uri == null) {
Toast.makeText(this, "Did not get a Uri??!?", Toast.LENGTH_LONG).show()
} else {
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
prefs.edit().putString(PREF_URI, uri.toString()).apply()
Toast.makeText(this, "OK, run the activity again", Toast.LENGTH_LONG).show()
}
finish()
}
}
I then ran the app containing this activity as its launcher activity and chose Downloads/ as the document tree. Subsequent launches of this activity — both before and after a reboot — show canRead() and canWrite() both return true. This was tested on a Google Pixel running Q Beta 6.
I cannot try this on a Q emulator because Google, so I cannot confirm if I can reproduce your behavior there. But, the fact that this works as expected on hardware should be a positive sign.
For those who appreciate the accepted answer, but do not want to use startActivityForResult, which was deprecated after the original answer was accepted.
My example shows how to save a file to storage from within your app.
First, create a class-wide variable to hold the ActivityResultLauncher information:
private lateinit var saveDataResultLauncher: ActivityResultLauncher<String>
Then, instead of using the deprecated feature, you can put this, for example, in your MainActivity's onCreate fun:
/** Backup Website Entries */
saveDataResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) {}
Make the user choose a location to save a file, after pressing a menu item bound to the functionality implemented above:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save_file -> {
saveDataResultLauncher.launch("example.txt")
true
}
else -> super.onOptionsItemSelected(item)
}
}