How can we access an expansion file in Android Q? - android

I'm writing an app that needs an expansion file and I want to ensure it will be compatible with Android Q. It seems the documentation provided does not address the changes in Android Q. In Android Q, getExternalStorageDirectory() won't be able to be used so how can we access the expansion file?

From the documentation linked to in the question, we know that an expansion file's name has the form:
[main|patch].<expansion-version>.<package-name>.obb
and the getObbDir() method returns the specific location for expansion files in the following form:
<shared-storage>/Android/obb/<package-name>/
So, the question is how do we access such files?
To answer this question, I have taken a directory containing five APK files and created an OBB file named "main.314159.com.example.opaquebinaryblob.obb" using JOBB. My intention is to mount and read this OBB file to display the APK file names and the count of entries in each APK (read as Zip files) in a small demo app.
The demo app will also try to create/read test files in various directories under the external storage directory.
The following was performed on a Pixel XL emulator running the latest available version of "Q" (Android 10.0 (Google APIs)). The app has the following characterisics:
targetSdkVersion 29
minSdkVersion 18
No explicit permissions
specified in the manifest
I peeked ahead to see what directory getObbDir() returns for this little app and found that it is
/storage/emulated/0/Android/obb/com.example.opaquebinaryblob
so I uploaded my OBB file to
/storage/emulated/0/Android/obb/com.example.opaquebinaryblob/main.314159.com.example.opaquebinaryblob.obb
using Android Studio. Here is where the file wound up.
So, can we mount and read this OBB file? Can we create/read files in other directories within the external files path? Here is what the app reports on API 29:
The only files that are accessible reside in /storage/emulated/0/Android/obb/com.example.opaquebinaryblob. Other files in the hierarchy cannot be either created or read. (Interestingly, though, the existence of these files could be determined.)
For the preceding display, the app opens the OBB file and reads it directly without mounting it.
When we try to mount the OBB file and dump its contents, this is what is reported:
Which is what we expect. In short, it looks like Android Q is restricting access to the external files directory while allowing targeted access based up the package name of the app.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var myObbFile: File
private lateinit var mStorageManager: StorageManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
obbDumpText.movementMethod = ScrollingMovementMethod()
val sb = StringBuilder()
val extStorageDir = Environment.getExternalStorageDirectory()
sb.appendln("getExternalStorageDirectory() reported at $extStorageDir").appendln()
myObbFile = File(obbDir, BLOB_FILE_NAME)
val obbDir = obbDir
sb.appendln("obbDir reported at $obbDir").appendln()
myObbFile = File(obbDir, BLOB_FILE_NAME)
val directoryPathList = listOf(
"$extStorageDir",
"$extStorageDir/Pictures",
"$extStorageDir/Android/obb/com.example.anotherpackage",
"$extStorageDir/Android/obb/$packageName"
)
var e: Exception?
for (directoryPath in directoryPathList) {
val fileToCheck = File(directoryPath, TEST_FILE_NAME)
e = checkFileReadability(fileToCheck)
if (e == null) {
sb.appendln("$fileToCheck is accessible.").appendln()
} else {
sb.appendln(e.message)
try {
sb.appendln("Trying to create $fileToCheck")
fileToCheck.createNewFile()
sb.appendln("Created $fileToCheck")
e = checkFileReadability(fileToCheck)
if (e == null) {
sb.appendln("$fileToCheck is accessible").appendln()
} else {
sb.appendln("e").appendln()
}
} catch (e: Exception) {
sb.appendln("Could not create $fileToCheck").appendln(e).appendln()
}
}
}
if (!myObbFile.exists()) {
sb.appendln("OBB file doesn't exist: $myObbFile").appendln()
obbDumpText.text = sb.toString()
return
}
e = checkFileReadability(myObbFile)
if (e != null) {
// Need to request READ_EXTERNAL_STORAGE permission before reading OBB file
sb.appendln("Need READ_EXTERNAL_STORAGE permission.").appendln()
obbDumpText.text = sb.toString()
return
}
sb.appendln("OBB is accessible at")
.appendln(myObbFile).appendln()
mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
obbDumpText.text = sb.toString()
}
private fun dumpMountedObb(obbMountPath: String) {
val obbFile = File(obbMountPath)
val sb = StringBuilder().appendln("Dumping OBB...").appendln()
sb.appendln("OBB file path is $myObbFile").appendln()
sb.appendln("OBB mounted at $obbMountPath").appendln()
val listFiles = obbFile.listFiles()
if (listFiles == null || listFiles.isEmpty()) {
Log.d(TAG, "No files in obb!")
return
}
sb.appendln("Contents of OBB").appendln()
for (listFile in listFiles) {
val zipFile = ZipFile(listFile)
sb.appendln("${listFile.name} has ${zipFile.entries().toList().size} entries.")
.appendln()
}
obbDumpText.text = sb.toString()
}
private fun checkFileReadability(file: File): Exception? {
if (!file.exists()) {
return IOException("$file does not exist")
}
var inputStream: FileInputStream? = null
try {
inputStream = FileInputStream(file).also { input ->
input.read()
}
} catch (e: IOException) {
return e
} finally {
inputStream?.close()
}
return null
}
fun onClick(view: View) {
mStorageManager.mountObb(
myObbFile.absolutePath,
null,
object : OnObbStateChangeListener() {
override fun onObbStateChange(path: String, state: Int) {
super.onObbStateChange(path, state)
val mountPath = mStorageManager.getMountedObbPath(myObbFile.absolutePath)
dumpMountedObb(mountPath)
}
}
)
}
companion object {
const val BLOB_FILE_NAME = "main.314159.com.example.opaquebinaryblob.obb"
const val TEST_FILE_NAME = "TestFile.txt"
const val TAG = "MainActivity"
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
tools:context=".MainActivity">
<TextView
android:id="#+id/obbDumpText"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
android:text="Click the button to view content of the OBB."
android:textColor="#android:color/black"
app:layout_constraintBottom_toTopOf="#+id/dumpMountObb"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside" />
<Button
android:id="#+id/dumpMountObb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="Dump\nMounted OBB"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/obbDumpText"
app:layout_constraintVertical_bias="0.79" />
</androidx.constraintlayout.widget.ConstraintLayout>
For a follow-up as stated here:
Since Android 4.4 (API level 19), apps can read OBB expansion files without external storage permission. However, some implementations of Android 6.0 (API level 23) and later still require permission, so you will need to declare the READ_EXTERNAL_STORAGE permission in the app manifest and ask for permission at runtime...
Does this apply to Android Q? It is not clear. The demo shows that it does not for the emulator. I hope that this is something that will be consistent across devices.

Related

FileObserver not working on Android SDK 30

I am working on an android application where i have used FileObserver to get data changes on a specific folder. The folder is WhatsApp Images and whenever there is a new file added to that folder i perform my further work when onEvent of FileObserver is triggered.
Everything works fine until i set targetSdkVersion=29 but as i am migrating my project to targetSdkVersion=30 FileObserver stopped working.
Below is my code for FileObserver
import android.os.Environment
import android.os.FileObserver
import android.util.Log
import java.io.File
class WhatsAppImageObserver11(var notify:(msg:String)->Unit) : FileObserver(
File(
Environment.getExternalStorageDirectory().toString(),
Constants.whatsapp_images_path11).toString(), ALL_EVENTS
) {
init {
Log.d("WhatsAppImageObserver11", "start")
}
override fun onEvent(i: Int, str: String?) {
val str2 = "WhatsAppImageObserver11"
if (i == CREATE || i == MOVED_TO && str != ".probe") {
val sb = StringBuilder()
sb.append("create File path--> ")
sb.append(str)
Log.d(str2, sb.toString())
try {
val whatsDeleted = File(
Environment.getExternalStorageDirectory().path,
Constants.whatsapp_reserved_media
)
if(!whatsDeleted.exists()) {
whatsDeleted.mkdirs()
}
val srcFile = File(
Environment.getExternalStorageDirectory(),
Constants.whatsapp_images_path11+str)
val destFile = File(Environment.getExternalStorageDirectory(), Constants.whatsapp_reserved_media+str)
if (srcFile.exists()){
srcFile.copyTo(target = destFile, overwrite = false, bufferSize = DEFAULT_BUFFER_SIZE)
}
} catch (e: Exception) {
val sb2 = StringBuilder()
sb2.append("create error: ")
sb2.append(e.toString())
Log.d(str2, sb2.toString())
}
}
if (i and 512 != 0 || i and 1024 != 0) {
val sb3 = StringBuilder()
sb3.append("dlete File path--> ")
sb3.append(str)
Log.d(str2, sb3.toString())
try {
val whatsDeleted = File(
Environment.getExternalStorageDirectory().path,
Constants.new_whatsapp_deleted_media
)
if(!whatsDeleted.exists()) {
whatsDeleted.mkdirs()
}
val srcFile = File(Environment.getExternalStorageDirectory().path, Constants.whatsapp_reserved_media+str)
val destFile = File(Environment.getExternalStorageDirectory().path, Constants.new_whatsapp_deleted_media+str)
if (srcFile.exists()){
srcFile.copyTo(target = destFile, overwrite = false, bufferSize = DEFAULT_BUFFER_SIZE)
srcFile.delete()
notify(destFile.absolutePath)
}
} catch (e2: Exception) {
val sb4 = StringBuilder()
sb4.append("del error: ")
sb4.append(e2.toString())
Log.d(str2, sb4.toString())
}
}
}
}
Here in this FileObserver i am copying media from WhatsApp Reserved Media to My own WhatsAppDeleted folder for media recovery feature.
What i have tried?
1- As i know Environment.getExternalStorageDirectory() is deprecated i tried replacing it with mContext.getExternalFilesDir(null).getAbsolutePath()
2- Also checked using android:preserveLegacyExternalStorage="true" in Manifest.
3- Tried added ignore attribute in
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage"/>
What is Required?
1- FileObserver on path "/Android/media/" + WHATSAPP_PKG_NAME + "/WhatsApp/Media/WhatsApp Images/" should trigger onEvent when i set targetSdkVersion-30
Can somebody please help me out with this? Any help will be appreciated.
Thank you
As written here:
Apps that run on Android 11 but target Android 10 (API level 29) can still request the requestLegacyExternalStorage attribute. This flag allows apps to temporarily opt out of the changes associated with scoped storage, such as granting access to different directories and different types of media files. After you update your app to target Android 11, the system ignores the requestLegacyExternalStorage flag.
Basically, you don't have permission to "listen" to external directories in the device, but only to your own internal/external directories.
You should use the MediaStore API in order to access media files which have been scanned by the OS (Including WhatsApp's files).
You can use components like JobService to register for the event of a new media been scanned.
Enjoy

How to parse an APK after converting it to a minimal one?

Background
I want to be able to parse APK files from various sources, or various kinds. I want to get only very specific, basic information about them:
package-name
version-code
version-name
label (app name)
icon
minSdkVersion
whether it's a split APK or not (available when parsing the manifest)
The Android framework has a single function to parse APK files (PackageManager.getPackageArchiveInfo), but it has 2 disadvantages:
It requires a file-path. This isn't always available, as you might have a Uri to deal with, or you have the APK file being inside some ZIP file.
It can't handle split APK files. Only the base one of them. If you try it on split APK files, you just get null.
Because of this, I've tried to find a reliable, Java&Kotlin solution that can handle them easily.
The problem
Each solution I've found has its own disadvatanges. Some libraries I couldn't even find how to use, and some don't have any way to parse APK files without having a real file path. Some of them just create new files instead of returning you objects in Java/Kotlin. Some are even in other programming languages, making me wonder if it's possible to use them.
So far, the only one that I've found as good enough, is "hsiafan" apk-parser, which needs only 2 files within the APK: manifest and resources files. It has some issues, but usually it can get you the information I've mentioned.
Thing is, as I wrote, it's not always working, so I want to return to the basics, at least when I notice it fails to parse, at least for the case that it's a normal APK of base of split-APK files. Some cases it couldn't handle well are:
Sometimes the app name couldn't be fetched correctly (here).
Some resources are somehow hidden in some apps (here). Even tools such as Jadx fail to show them, but others can.
Adaptive icon is hard to parse, including drawable XML files. I succeed parsing VectorDrawable though (here), but it's quite a workaround.
And a few others.
What I've tried
So, I wanted to try out the Android framework again, but this time with a new idea: instead of handling the entire original APK, I could extract only what it really needs, based on the current observation of it.
For example, if I know the resources I need to handle, I can copy only them (and some key files that are needed) into a new APK, and ditch the rest. This could reduce the need to copy/download huge amount of data just to parse the APK and get information about it. If the APK file is 100MB, for example, there is no need to get it all when all we need is just a tiny portion of it.
As a start, I wanted to see how I can create a new APK that can be parsed, so for now I copied all of the entries of the original APK (currently of the current app) into a new file, and chose to parse it:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null)
return
thread {
val originalApplicationInfo = packageManager.getApplicationInfo(packageName, 0)
val filePath = originalApplicationInfo.publicSourceDir
val outputFile = File(cacheDir, "test.apk")
outputFile.parentFile!!.mkdirs()
outputFile.delete()
ZipFile(filePath).use { zipFile ->
ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
for (entry in zipFile.entries()) {
val name = entry.name
zipOutputStream.putNextEntry(ZipEntry(name))
zipFile.getInputStream(entry).use { it.copyTo(zipOutputStream.buffered()) }
zipOutputStream.closeEntry()
}
}
}
val originalLabel = originalApplicationInfo.loadLabel(packageManager)
val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
Log.d("AppLog", "originalPackageInfo: label:$originalLabel appIcon:${originalIcon?.javaClass?.simpleName}")
//
val packageArchiveInfo = packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
val label = packageArchiveInfo?.applicationInfo?.loadLabel(packageManager)?.toString()
val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
Log.d("AppLog", "packageArchiveInfo!=null?${packageArchiveInfo != null} label:$label appIcon:${appIcon?.javaClass?.simpleName}")
}
}
}
The file indeed gets generated, but for some reason the Android framework failed to parse it, as packageArchiveInfo is null.
The questions
How come the sample I've made doesn't work? How come the new APK can't be parsed?
What is the minimal set of files the getPackageArchiveInfo function requires to be able to parse the APK, just for the information I've mentioned above?
If this kind of solution doesn't work, is there perhaps a library that could handle APK files of all kinds with all the information I've mentioned, no matter the source (including Uri and within zip files)?
Edit: as suggested, I could copy just those of folder "AndroidManifest.xml", "resources.arsc", "res" , but it seems it can still not always work well, as the icons don't always get to be the same:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null)
return
thread {
val installedApplications = packageManager.getInstalledPackages(0)
Log.d("AppLog", "checking ${installedApplications.size} apps")
for (originalPackageInfo in installedApplications) {
val originalApplicationInfo = originalPackageInfo.applicationInfo
val filePath = originalApplicationInfo.publicSourceDir
val outputFile = File(cacheDir, "test.apk")
outputFile.parentFile!!.mkdirs()
outputFile.delete()
val toExtract = setOf<String>("AndroidManifest.xml", "resources.arsc", "res")
ZipFile(filePath).use { zipFile ->
ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
for (entry in zipFile.entries()) {
val name = entry.name
if (toExtract.contains(name.split("/")[0])) {
zipOutputStream.putNextEntry(ZipEntry(name))
zipFile.getInputStream(entry).use { inStream ->
zipOutputStream.buffered().apply {
inStream.copyTo(this)
}.flush()
}
}
}
}
}
val packageName = originalApplicationInfo.packageName
val originalLabel = originalApplicationInfo.loadLabel(packageManager)
val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
val originalIconBitmap = originalIcon?.toBitmap()
//
val packageArchiveInfo = packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
if (packageArchiveInfo == null) {
Log.e("AppLog", "$packageName could not parse generated APK")
continue
}
val label = packageArchiveInfo.applicationInfo.loadLabel(packageManager).toString()
val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
val appIconBitmap = appIcon?.toBitmap()
when {
label != originalLabel ->
Log.e("AppLog", "$packageName got wrong label $label vs $originalLabel")
packageArchiveInfo.versionName != originalPackageInfo.versionName ->
Log.e("AppLog", "$packageName got wrong versionName ${packageArchiveInfo.versionName} vs ${originalPackageInfo.versionName}")
packageArchiveInfo.versionCode != originalPackageInfo.versionCode ->
Log.e("AppLog", "$packageName got wrong versionCode ${packageArchiveInfo.versionCode} vs ${originalPackageInfo.versionCode}")
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageArchiveInfo.applicationInfo.minSdkVersion != originalApplicationInfo.minSdkVersion ->
Log.e("AppLog", "$packageName got wrong minSdkVersion ${packageArchiveInfo.applicationInfo.minSdkVersion} vs ${originalApplicationInfo.minSdkVersion}")
appIcon?.javaClass?.name != originalIcon?.javaClass?.name ->
Log.e("AppLog", "$packageName got different app icon type: ${appIcon?.javaClass?.simpleName} vs ${originalIcon?.javaClass?.simpleName}")
originalIconBitmap != null && appIconBitmap != null && (originalIconBitmap.width != appIconBitmap.width || originalIconBitmap.height != appIconBitmap.height) ->
Log.e("AppLog", "$packageName got wrong app icons sizes:${appIconBitmap.width}x${appIconBitmap.height} vs ${originalIconBitmap.width}x${originalIconBitmap.height}")
originalIconBitmap != null && appIconBitmap != null && !areBitmapsSame(originalIconBitmap, appIconBitmap) ->
Log.e("AppLog", "$packageName got wrong app icons content ")
(originalIconBitmap == null && appIconBitmap != null) || (originalIconBitmap != null && appIconBitmap == null) ->
Log.e("AppLog", "$packageName null vs non-null app icon: ${appIconBitmap != null} vs ${originalIconBitmap != null}")
}
}
Log.d("AppLog", "done")
}
}
fun areBitmapsSame(bitmap: Bitmap, bitmap2: Bitmap): Boolean {
if (bitmap.width != bitmap2.width || bitmap.height != bitmap2.height)
return false
for (x in 0 until bitmap.width)
for (y in 0 until bitmap.height)
if (bitmap.getPixel(x, y) != bitmap2.getPixel(x, y))
return false
return true
}
}
I think that since app icons are very complex and depend on various resources (which might even be hidden in weird ways), there is no other choice than to actually have the file on the file system.
EDIT: about getting the app icons, I actually didn't use it well for the APK file. I used something a bit different on my own app, which seems to work well here too, except for some cases that the icon is a bit different (shape/color might be different for example), probably due to different configurations. But at least it won't return you the default app icon of Android when the app clearly has a non-default app icon.
Sadly though, it didn't always get good app icon when creating the minimized APKs (sometimes returned me ColorStateListDrawable or ColorDrawable, for example), probably because sometimes the original APK has hidden resources in non-conventional paths. So this is how you can get the app icon, assuming you have the whole APK:
Before getting it, use:
packageArchiveInfo.applicationInfo.publicSourceDir = targetFilePath
packageArchiveInfo.applicationInfo.sourceDir = targetFilePath
And then call this function:
fun getAppIcon(context: Context, applicationInfo: ApplicationInfo): Drawable? {
val packageManager = context.packageManager
try {
val iconResId = applicationInfo.icon
if (iconResId != 0) {
val resources: Resources = packageManager.getResourcesForApplication(applicationInfo)
val density = context.resources.displayMetrics.densityDpi
var result = ResourcesCompat.getDrawableForDensity(resources, iconResId, density, null)
if (result != null)
return result
}
} catch (e: Exception) {
// e.printStackTrace()
}
try {
val applicationIcon = packageManager.getApplicationIcon(applicationInfo)
// Log.d("AppLog", "getApplicationIcon type:${applicationIcon.javaClass.simpleName}")
return applicationIcon
} catch (ignored: Exception) {
}
return null
}
To convert to Bitmap, you can use:
val appIconBitmap = try {
appIcon?.toBitmap(appIconSize, appIconSize)
} catch (e: Exception) {
e.printStackTrace()
null
}
And to get the app icon size, you can use:
fun getAppIconSize(context: Context): Int {
val activityManager = context.getSystemService<ActivityManager>()
val appIconSize = try {
activityManager.launcherLargeIconSize
} catch (e: Exception) {
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, context.resources.displayMetrics).toInt()
}
return appIconSize
}
Change the copy code from
zipFile.getInputStream(entry).use { it.copyTo(zipOutputStream.buffered()) }
to
zipFile.getInputStream(entry).use {
val bufStream = zipOutputStream.buffered()
it.copyTo(bufStream)
bufStream.flush()
}
or something similar to make sure that all the data gets written out.
I thought that the APK was failing certificate verification, but that is not the case. The foregoing will get you the label and the icon.
With the original code I was seeing the following error
chunk size is bigger than given data
Failed to load 'resources.arsc' in APK '/data/user/0/com
which indicates to me that something went awry with the output. With the aforementioned change, I see the following:
originalPackageInfo: label:APK Copy and Read appIcon:AdaptiveIconDrawable
packageArchiveInfo!=null?true label:APK Copy and Read appIcon:AdaptiveIconDrawable
If I open the created APK from the Device Explorer in Android Studio, it parses out nicely.
As for the minimum, I think, for your purposes, you will need the manifest, the resource.arsc file and the res directory. Here is how to get a reduced APK that will parse with just these elements:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null)
return
thread {
val originalApplicationInfo = packageManager.getApplicationInfo(packageName, 0)
val filePath = originalApplicationInfo.publicSourceDir
val outputFile = File(cacheDir, "test.apk")
outputFile.parentFile!!.mkdirs()
outputFile.delete()
val toExtract = setOf<String>("AndroidManifest.xml", "resources.arsc","res")
ZipFile(filePath).use { zipFile ->
ZipOutputStream(FileOutputStream(outputFile)).use { zipOutputStream ->
for (entry in zipFile.entries()) {
val name = entry.name
if (toExtract.contains(name.split("/")[0])) {
zipOutputStream.putNextEntry(ZipEntry(name))
zipFile.getInputStream(entry).use { inStream ->
zipOutputStream.buffered().apply {
inStream.copyTo(this)
}.flush()
}
}
}
}
}
val originalLabel = originalApplicationInfo.loadLabel(packageManager)
val originalIcon: Drawable? = originalApplicationInfo.loadIcon(packageManager)
Log.d(
"AppLog",
"originalPackageInfo: label:$originalLabel appIcon:${originalIcon?.javaClass?.simpleName}"
)
//
val packageArchiveInfo =
packageManager.getPackageArchiveInfo(outputFile.absolutePath, 0)
val label = packageArchiveInfo?.applicationInfo?.loadLabel(packageManager)?.toString()
val appIcon = packageArchiveInfo?.applicationInfo?.loadIcon(packageManager)
Log.d(
"AppLog",
"packageArchiveInfo!=null?${packageArchiveInfo != null} label:$label appIcon:${appIcon?.javaClass?.simpleName}"
)
}
}
}

Android storage access framework delete issue

I'm working with SAF (Storage access framework) to write down video files onto SD-CARD.
I've successfully create files, read them and moving.
But i have found the issue with deleting files.
When i delete files using SAF. Files are gone, but space isn't recover.
For example: when 1 have 2GB left out of 10GB.
And i delete file (1GB total) space left is 2GB, not 3GB.
I've used: enter code here
val file: DocumentFile = DocumentFile.fromTreeUri(ctx, treeUri)
file?.delete()
Even:
DocumentsContract.deleteDocument(
activity.contentResolver,
uri
)
and even this:
val path = "/storage/${external?.name}/root.img"
val f = File(path)
if (f.exists()) {
val deleteCmd = "rm -r $path"
val runtime = Runtime.getRuntime()
try {
runtime.exec(deleteCmd)
} catch (e: IOException) {
e.printStackTrace()
}
}
app.applicationContext.deleteFile(f.name)
f.absoluteFile.delete()
f.exists()

How to access external storage in Android 10?

As per Android 10 update, Reading and writing from external storage is restricted even we have
WRITE_EXTERNAL_STORAGE
I want to move my pdf file from internal storage (private of my android app) to DOWNLOADS folder. I tried so many approaches but nothing worked and there is no any help available on internet related to accessing external storage in android 10.
Privacy changes in Android 10  |  Android Developers
https://developer.android.com
hope below code will help you to resolve your issue and copy any file from internal to external storage.
fun copyPdfFrom(context: Context, sourceFile: File, destinationURIString: String) {
val destinationURI = Uri.parse(destinationURIString)
try {
val bufferedInputStream = BufferedInputStream(FileInputStream(sourceFile.absoluteFile))
val outPutStream = context.contentResolver.openOutputStream(destinationURI)!!
var len = 0
val brr = ByteArray(1024)
while ((bufferedInputStream.read(brr, 0, brr.size).also { len = it }) != -1) {
outPutStream.write(brr, 0, len)
}
outPutStream.flush()
outPutStream.close()
bufferedInputStream.close()
}
catch (e: Exception) {
e.printStackTrace()
}
}

Downloading file and saving it to download folder using kotlin in Android

I am trying to download an apk file from the web server and store it in the download folder. I am using fuel library
Fuel.download("https://mypath/app.apk").destination {
response, url -> val dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()) File(dir, "abcd.apk")
}.progress {
readBytes, totalBytes -> val progress = readBytes.toFloat() / totalBytes.toFloat()
Log.d("log",progress.toString())
}.response {
req, res, result -> Log.d("log","download completed ")
Log.d("log",Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString() + "/abcd.apk")
}
I am getting bytesArray in response but the file is not saving to download folder. I have write_external_storage on my Manifest file. Please let me know what am I missing?
As you mentioned in comment that you are using targetSdkVersion as 23. Beginning with Android 6.0 (API level 23), if you set targetSdkVersion as 23 in your app you have to implement rumtime permission in your app.
To implement it please refer official documentation here Requesting Permissions at Run Time
fun storeImage(link: String) : String {
val policy: StrictMode.ThreadPolicy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
val cw = ContextWrapper(applicationContext)
val directory: File = cw.getDir("imageDir", Context.MODE_PRIVATE)
val pictureFile = File(directory, "backgroundImage.gif")
URL(link).openStream().use { input ->
FileOutputStream(pictureFile).use { output ->
input.copyTo(output)
Log.e("DD GIF", "storeImage: FILE SAVED" )
return "SAVED"
}
}
}
it is working for me.

Categories

Resources