My app is a cloud storage on target SDK 30, I want to download folders in API 30 but it doesn't work.
At first, the user is directed to the SAF environment using ACTION_CREATE_DOCUMENT, after selecting the path, the files are downloaded in the App Data path and then copied to the path selected by the user.
fun openSavePickerForDirectories(activity: Activity) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.type = DocumentsContract.Document.MIME_TYPE_DIR
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.putExtra(
Intent.EXTRA_TITLE, "Export folder"
)
try {
activity.startActivityForResult(
intent,
ErrorCodes.FILE_STORAGE_UTILS_OPEN_SAVE_PICKER_DIRECTORY
)
} catch (e: Exception) {
if (e.toString().contains("ActivityNotFoundException"))
activity.toast(activity.getString(R.string.filemanager_not_found))
e.printStackTrace()
}
}
#Throws(IOException::class)
fun copyDirectory(sourceLocation: File, targetLocation: File) {
if (sourceLocation.isDirectory) {
if (!targetLocation.exists() && !targetLocation.mkdirs()) {
throw IOException("Cannot create dir " + targetLocation.absolutePath)
}
val children = sourceLocation.list()
for (i in children.indices) {
copyDirectory(
File(sourceLocation, children[i]),
File(targetLocation, children[i])
)
}
} else {
copyFile(sourceLocation, targetLocation)
}
}
Related
I save a png image to external storage using this block of code for sdk<=28
/**
* save image with this method if the sdk is 28 or lower
*/
private fun saveImageSdk28(fileName: String){
//declar the output stream variable outside of try/catch so that it can always be closed
var imageOutputStream: FileOutputStream? = null
var outputImageFile = getFile(fileName)
if (!outputImageFile.exists()) {
outputImageFile.createNewFile()
}
try {
imageOutputStream = FileOutputStream(outputImageFile)
encryptedBitmap.compress(Bitmap.CompressFormat.PNG, 100, imageOutputStream)
} catch (e: IOException) {
e.printStackTrace()
Timber.i(e.toString())
} finally {
if (imageOutputStream != null) {
imageOutputStream.flush()
imageOutputStream.close()
}
}
}
/**
* returns file from fileName
*/
fun getFile(fileName: String): File{
//open, or create the directory where the image will be stored
var directory = File(
Environment.getExternalStorageDirectory().toString() + "/AppNameOutput/"
)
if (!directory.exists()) {
directory.mkdir()
}
//create the file
var file: File = File(directory.absolutePath, fileName)
return file
}
and this code for when the sdk>28
/**
* save image with this method if the sdk is 29 or higher
*/
#RequiresApi(Build.VERSION_CODES.Q)
private fun saveImageSdk29(fileName: String){
val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$fileName")
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
put(MediaStore.Images.Media.WIDTH, encryptedBitmap.width)
put(MediaStore.Images.Media.HEIGHT, encryptedBitmap.height)
}
try{
val contentResolver = getApplication<Application>().contentResolver
contentResolver.insert(imageCollection, contentValues)?.also {uri->
contentResolver.openOutputStream(uri).use {outputStream ->
encryptedBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
}
}catch (e: IOException){
e.printStackTrace()
}
}
The image sucsessfully saves on the users device and can be accesed through files, however, the user can't access these images through the gallery, or Images tab.
I solved it. Turns out you just need to wait a while and reboot the phone for the gallery to show your images.
I was working on an update for my app, a section of which deals with downloading and installing an APK file.
As long as the previous version were targeting SDK 30 everything worked pretty smoothly. But as soon as I incremented the target and compile SDK to 32 it just started behaving queerly.
Here is the code that deals which the package manager and the installation:
private fun installOnClickListener() {
binding.termuxInstallCard.showProgress()
var session: PackageInstaller.Session? = null
try {
val packageInstaller: PackageInstaller =
requireContext().packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
viewModel.addApkToSession(session)
var installBroadcast: PendingIntent? = null
val intent =
Intent(PACKAGE_INSTALLED_ACTION).putExtra(
"packageName",
"com.termux"
)
installBroadcast = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
0,
intent,
FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT)
}
session.commit(installBroadcast.intentSender)
session.close()
} catch (e: IOException) {
throw RuntimeException("Couldn't install package", e)
} catch (e: RuntimeException) {
session?.abandon()
throw e
} finally {
session?.close()
}
}
Here is what is happening:
As I am targeting SDK 32, I am required to specify the Mutability of PendingIntent
Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent
When I use:
FLAG_MUTABLE- The installation just fails stating the error code- STATUS_FAILURE_INVALID with no extra message for debugging in EXTRA_STATUS_MESSAGE. The thing is that when I try to install the same downloaded APK via the adb shell, it just installs normally without any issues.
FLAG_IMMUTABLE- The installation succeeds without prompting user with the installation dialog but nothing is actually installed.
More code in case you need it-
fun addApkToInstallSession(
path: String,
session: PackageInstaller.Session
) {
val file = File("${context.filesDir.path}/$path")
val packageInSession: OutputStream = session.openWrite("com.termux", 0, -1)
val inputStream = FileInputStream(file)
val byteStream = inputStream.read()
try {
var c: Int
val buffer = ByteArray(16384)
while (inputStream.read(buffer).also { c = it } >= 0) {
packageInSession.write(buffer, 0, c)
}
} catch (e: IOException) {
println("IOEX")
} finally {
try {
packageInSession.close()
inputStream.close()
} catch (e: IOException) {
println("IOEX in closing the stream")
}
}
}
private val broadcastReceiverForInstallEvents = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
lifecycleScope.launch(Dispatchers.IO) {
val extras = intent.extras
val status = extras!!.getInt(PackageInstaller.EXTRA_STATUS)
val packageName = extras.getString("packageName")!!
if (PACKAGE_INSTALLED_ACTION == intent.action) {
println("STATUS $status")
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
try {
val confirmIntent = extras[Intent.EXTRA_INTENT] as Intent
confirmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(confirmIntent)
} catch (e: Exception) {
lifecycleScope.launch(Dispatchers.Main) {
Toast.makeText(
requireContext(),
"We could not find an application to handle the installation of apps. Please download a package installer.",
Toast.LENGTH_SHORT
).show()
}
}
}
PackageInstaller.STATUS_SUCCESS -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install succeeded!")
// todo all done animation
binding.termuxInstallCard.markAsComplete()
Toast.makeText(requireContext(), "All Done!", Toast.LENGTH_SHORT)
.show()
lifecycleScope.launch {
// viewModel.setTermuxSetupDone()
}
/* redirecting... */
Handler(Looper.getMainLooper()).postDelayed({
redirect()
}, 2000)
}
}
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
lifecycleScope.launch(Dispatchers.Main) {
println("Extra Status Message${extras.getString("EXTRA_STATUS_MESSAGE")}")
"There was an error installing Termux. Please retry.".showSnackbar(
binding.root,
true
)
binding.termuxInstallCard.hideProgress()
}
}
else -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install failed else!")
// exitActivity("Package failed to install -> Unknown Error!")
binding.termuxInstallCard.hideProgress()
}
}
}
}
}
}
}
I would really appreciate some help!
In the past when I tried to install APKs I used either Knox on Samsung devices or I'd refer the user to install the package via package manager, this is the code I've used:
public static void cleanInstall(String datum, Context context) {
File file = new File(datum);
if (file.exists()) {
Intent intent = new Intent(Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri downloadedApk = FileProvider.getUriForFile(context, ".MyPackage.myFileProvider", file);
intent.setDataAndType(downloadedApk, type);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(file), type);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} else {
Toast.makeText(context, "ّFile not found!", Toast.LENGTH_SHORT).show();
}
}
I've not used this code in a while so I don't know if it works anymore but it's something you might want to check, to see if it does. Make sure the path you are referring to does not reside on SD as you'll probably face permission denied issues.
On calculating the hash of the file I was committing to the session of the Package Manager, I found out that it didn't write the APK file correctly.
val inputStream: InputStream = session.openRead("com.termux")
val file = File("${requireContext().filesDir.path}/test2.apk")
if (!file.exists()) {
file.createNewFile()
}
try {
inputStream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
println("Exception occured $e")
}
if (file.length() > 0) {
val hash = file.getMD5Hash()
println("HASH of session - $hash")
}
Fixed that and with the combination of the Mutable pending intent. The package is now installing perfectly.
I am trying to capture video on my app. It works below android API 30 but does not work on 30+. Seems like after sdk 30, android does not allow to read external storage entirely (scoped storage). I am currently having this error:
java.lang.IllegalStateException: Only owner is able to interact with pending item content://media/external_primary/video/media/57
Now I have three questions:
How can I create video capture intent that saves video to apps internal storage? (Because scoped storage limitations are for external storage)
I can get content uri at onActivityResult, how to make this uri accessible and readable? (After I read this file, I will create a temporary file with it and edit this temp file.)
What is the proper way to capture a video with scoped storage limitations?
video capture intent
private fun dispatchTakeVideoIntent() {
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
takeVideoIntent.resolveActivity(packageManager)?.also {
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE)
}
}
}
onActivityResult
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK){
when(requestCode){
REQUEST_VIDEO_CAPTURE -> {
val videoUri: Uri? = data?.data
setVideo(videoUri)
}
}
}
}
videoUri looks like this: content://media/external_primary/video/media/57
setVideo function normally gets the content uri, creates a temporary file from it, compresses, and gets a thumbnail from this file. And then I upload this file to the server.
Thanks to #CommonsWare s advice, I created a file with File provider and supply uri of this file with EXTRA_OUTPUT. Now I am able to do stuff with videoUriForAddingCaptureVideo and videoPathForAddingCaptureVideo variables. I am posting this answer to give a clue to fellow developers.
private fun dispatchTakeVideoIntent() {
val videosFolder = File(
Environment
.getExternalStorageDirectory(), application.applicationContext.resources
.getString(R.string.app_name)
)
try {
if (!videosFolder.exists()) {
val isCreated: Boolean = videosFolder.mkdirs()
if (!isCreated) {
Log.e(TAG,"dispatchTakeVideoIntent : storage error")
return
}
}
} catch (e: Exception) {
e.printStackTrace()
}
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val videoFileName = "VID_" + timeStamp + "_"
val storageDir: File? = application.applicationContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
try {
val video = File.createTempFile(
videoFileName, /* prefix */
".mp4", /* suffix */
storageDir /* directory */
)
videoUriForAddingCaptureVideo = FileProvider.getUriForFile(application.applicationContext, application.applicationContext.packageName + ".provider", video)
videoPathForAddingCaptureVideo = video.absolutePath //Store this path as globe variable
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUriForAddingCaptureVideo)
takeVideoIntent.resolveActivity(packageManager)?.also {
startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE)
}
}
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
I am struggling on finding a way to save my image on DCIM folder since MediaStore.Images.Media.RELATIVE_PATH is only available on android 10 and up
Here is a snippet
var imageCollection = sdk29AndUp {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} ?: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val randomName = UUID.randomUUID().toString()
val folderName = context.getString(R.string.output_directory)
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$randomName.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
// store in a sub folder on android 10 and up
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/$folderName")
} else {
// TODO: How do i set the folder for android 9 and lower
}
}
try {
context.contentResolver.insert(imageCollection, contentValues)?.also { uri ->
cameraLauncher.launch(uri)
}
} catch (e: Exception) {
e.printStackTrace()
// TODO: Notifiy the user there is an error creating the URI
}
I am downloading file from server ,and saving to download folder in android ,accessing file path by following way to save
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
is depreceated in Android Q ,So I have to use Storage access framework .
fun downloadFile(url: String, originalFileName: String) {
mDownloadDisposable.add(
mRCloudRepository.downloadFile(url)
.flatMap(object : Function1<Response<ResponseBody>, Flowable<File>> {
override fun invoke(p1: Response<ResponseBody>): Flowable<File> {
return Flowable.create({
try {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absoluteFile, originalFileName)
val sink = file.sink().buffer()
sink.writeAll(p1.body()!!.source())
sink.close()
it.onNext(file)
it.onComplete()
} catch (e: IOException) {
e.printStackTrace()
it.onError(e)
}
}, BackpressureStrategy.BUFFER)
}
})
.subscribeOn(mIoScheduler)
.observeOn(mUiScheduler)
.subscribe(
{
cancelNotification()
val contentIntent: PendingIntent = PendingIntent.getActivity(mContext, NotificationBuilder.NOTIFICATION_DOWNLOAD_ID, getOpenFileIntent(it), PendingIntent.FLAG_CANCEL_CURRENT)
NotificationBuilder.showDownloadedNotification(mContext!!, 2, "Downloaded $originalFileName ", "", contentIntent)
}, {
cancelNotification()
it.printStackTrace()
}
)
)
}
You can try DocumentFileCompat#createDownloadWithMediaStoreFallback() in Simple Storage:
val uri = DocumentFileCompat.createDownloadWithMediaStoreFallback(applicationContext, FileDescription(filename))
val output = uri?.openOutputStream(applicationContext)
val input = // get input stream from Response<ResponseBody>
// then, write input stream to output stream
In Android 10, accessing files in Download directory requires URI, instead of direct file path.