Related
For my Android native application, I need to pick an image from the gallery and save it local app folder. I run the intent to open the image picker and select the image without any problem. After this the callback of registerForActivityResult is called and I get the path of the image. Now I use this code to recreate the bmp and save it locally:
val stream = contentResolver.openInputStream(result.data!!.data!!)
val bm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val bmpSource = ImageDecoder.createSource(
applicationContext.contentResolver,
result.data!!.data!!
)
ImageDecoder.decodeBitmap(
bmpSource
)
} else {
MediaStore.Images.Media.getBitmap(
contentResolver,
result.data!!.data!!
)
}
bm!!.saveToLocalStorage(this, "AVATAR_${mainViewModel.selectedDevice.first()!!.qrCode}.jpg")
fun Bitmap.saveToLocalStorage(context: Context, filename: String): Boolean {
return try {
context.openFileOutput("$filename", MODE_PRIVATE).use { stream ->
if (!this.compress(Bitmap.CompressFormat.JPEG, 90, stream)) {
throw IOException("Couldn't save image")
}
stream.close()
}
true
} catch (ex: IOException) {
false
}
}
I tried this code and it works properly on my Samsung (Android 11). The same code gives me problems on a Xiaomi device with Android 10. I thought it could be a problem with the version of Android, but in another device with Android 10 it works. The problem I'm facing in the Xiaomi device is that the image stored in the local app folder is just a black image with few Kb. The problems seems in the decode of the image and not in the saving method. Why do I get this problem? What is the best solution to copy the image locally?
EDIT: I cannot simply copy the picked image in the local app folder since I have to change its size. So I should load the image in memory, then create a scaled bitmap and finally save it to local app folder.
I have an app which records videos to shared MOVIES folder.
I can delete those files on Android 11 (API 30) with contentResolver.delete(uri, null, null) method in my recorded videos activity.
But if I reinstall the app then it looses permissions to those files... (so afwul) and in such case I need to do something like this:
try {
context.contentResolver.delete(uri, null, null)
} catch (exception: Exception) {
if (exception is RecoverableSecurityException) {
val intentSender = exception.userAction.actionIntent.intentSender
intentSender?.let {
callback?.startIntentSenderForResult(
intentSender,
requestCode
)
}
}
}
So it couldn't delete the file using ContentResolver because app was reinstalled and there is exception which we can catch and open the next annoying dialog for a user to confirm deletion (and for each file deletion it should be a different dialog, multiple deleting - no way)
Then I installed Explorer app from Google Play on this Android 11 device (emulator), when I opened it the app only asked for storage write permission (my app also does it) and this Explorer app could easily delete any file (including my record videos files) without any confirmation dialog.
So how do they do it? Is it a hack or what is that?
Link to the app https://play.google.com/store/apps/details?id=com.speedsoftware.explorer
Update
VLC for Android can also delete any media file https://play.google.com/store/apps/details?id=org.videolan.vlc
They also use content provider, so it's the same but it returns true unlike my app, why?
fun deleteFile(file: File): Boolean {
var deleted: Boolean
//Delete from Android Medialib, for consistency with device MTP storing and other apps listing content:// media
if (file.isDirectory) {
deleted = true
for (child in file.listFiles()) deleted = deleted and deleteFile(child)
if (deleted) deleted = deleted and file.delete()
} else {
val cr = AppContextProvider.appContext.contentResolver
try {
deleted = cr.delete(MediaStore.Files.getContentUri("external"),
MediaStore.Files.FileColumns.DATA + "=?", arrayOf(file.path)) > 0
} catch (ignored: IllegalArgumentException) {
deleted = false
} catch (ignored: SecurityException) {
deleted = false
}
// Can happen on some devices...
if (file.exists()) deleted = deleted or file.delete()
}
return deleted
}
https://github.com/videolan/vlc-android/blob/master/application/vlc-android/src/org/videolan/vlc/util/FileUtils.kt#L240
Android 11 (API 30) without system confirmation dialog you can do but you need to manage_external_storage permission. The permission is allowed to some specific category application.
File managers
Backup and restore apps
Anti-virus apps
Document management apps
On-device file search
Disk and file encryption
Device-to-device data migration
Manage all files on a storage device
If your app do not follow the above category so you do not allow to publish with manage_external_storage permission.
If your application is a gallery, video, and audio player so you do not need to manage_external_storage permission and you can delete it directly with the system confirmation dialog.
Here you can get the example to delete media file
Before android 11 you can direct the used file.delete() method and delete your file.
In the android 11 file.delete() method only works if you create your own content. for example, Your application download one image and the location is external storage in this casework file.delete() method.
if you want to delete media files like camera or screenshot that time file.delete() method not work in android 11 because the media content you are not created. This situation follows with a system confirmation dialog.
Well here is a good solution for Android Q and R,
You can rename, duplicate or delete the file in Android R.
Check this class: https://gist.github.com/fiftyonemoon/433b563f652039e32c07d1d629f913fb
below is a code snippet that works for all android versions till Android 11
fun deleteFile(path_of_file :String){
val uri = Uri.parse(path_of_file)
try{
// android 28 and below
contentResolver.delete(uri, null, null)
}catch (e : SecurityException){
// android 29 (Andriod 10)
val intentSender = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
MediaStore.createDeleteRequest(contentResolver, listOf(uri)).intentSender
}
// android 30 (Andriod 11)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
val recoverableSecurityException = e as? RecoverableSecurityException
recoverableSecurityException?.userAction?.actionIntent?.intentSender
}
else -> null
}
intentSender?.let { sender ->
intentSenderLauncher.launch(
IntentSenderRequest.Builder(sender).build()
)
}
}
}
There is ContentResolver.applyBatch() which will only once ask the user i think.
Have a look here:
https://developer.android.com/guide/topics/providers/content-provider-basics.html#Batch
Crash : java.lang.IllegalArgumentException Volume external_primary not found
When querying for tracks from media store, I am getting this crash in some Android 10 devices (Most of them are from Xiaomi Mi A2 Lite, Motorola, HMD Global Nokia).
Cursor cursor = getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
Should I be passing a different URI for Android 10 devices ?. (However it is working fine in most of the devices )
Stack trace :
Caused by java.lang.IllegalArgumentException: Volume external_primary not found
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:170)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.query(ContentProviderProxy.java:423)
at android.content.ContentResolver.query(ContentResolver.java:946)
at android.content.ContentResolver.query(ContentResolver.java:881)
at android.content.ContentResolver.query(ContentResolver.java:837)
at com.example.musicplayer.CursorFactory.getAllSongsCursor(CursorFactory.java:164)
Edit : Based on this issue reported, Suspect it could be an issue with sdcard in those devices with Android 10 OS.
Post Android 10, API 28, there are some changes when accessing the media content from other sources.
Google mentioned it in this link https://developer.android.com/training/data-storage/shared/media#storage-volume
You can get more information on how to resolve this issue with more information given by google with the below code:
// Add a specific media item.
ContentResolver resolver = getApplicationContext()
.getContentResolver();
// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
Uri audioCollection = MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
// Publish a new song.
ContentValues newSongDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
"My Song.mp3");
// Keeps a handle to the new song's URI in case we need to modify it
// later.
Uri myFavoriteSongUri = resolver
.insert(audioCollection, newSongDetails);
Hope this works for you!
Google added a thing called Scoped Storage which changes the File operation a bit. so you should use VOLUME_EXTERNAL/VOLUME_EXTERNAL_PRIMARY instead of EXTERNAL_CONTENT_URI
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
Going with VOLUME_EXTERNAL/VOLUME_EXTERNAL_PRIMARY is the right direction but there is a really rare case that the user stores their audio file on SD card but the phone doesn't recognize it which leads to the crash. This is a device error and there is nothing you can do about it.
From the doc of https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL_PRIMARY
you could use getExternalVolumeNames to check is there any external storage available before making the query.
https://developer.android.com/reference/android/provider/MediaStore#getExternalVolumeNames(android.content.Context)
You need to ask read permission for Android Q and above to access files outside of your application scope.
As per documentation mention as below and here is the documention link
If scoped storage is enabled, the collection shows only the photos, videos, and audio files that your app has created. Most developers won't need to use MediaStore.Files to view media files from other apps, but if you have a specific requirement to do so, you can declare the READ_EXTERNAL_STORAGE permission. It's recommended, however, that you use the MediaStore APIs to open files that your app hasn't created.
Here is the common code to ask permission for all version,
Declare the request launcher object
private val requestReadResult = registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
if (result) {
queryData()
} else {
AlertDialog.Builder(this)
.setMessage(R.string.app_permission_required)
.setPositiveButton(R.string.exit) { _, _ -> finish() }.setCancelable(false).create().show()
}
}
Check permission granted or not before query
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
requestReadResult.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
return
}
queryData()
Now check for the Android SDK version and as per that initialize your Uri
val uri= if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
else
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// Here you will getting all the Images from device.
val mediaCursor = contentResolver.query(uri, null, null, null, null)
As per your media requirement change MediaStore.yourmediatype and use it.
Thanks
In Android Q, apps that aren't the default file manager or gallery can only modify and/or delete image files which they own, so, which an app has created.
Granting the read/write permissions doesn’t allow to modify or to delete any file that isn’t owned by an app.
This implies that not only files created by other apps are out of reach, but also if an app gets uninstalled and then reinstalled, then this one loses the ownership over all the public files that the app previously created. So, after the re-installation it can’t modify or delete them anymore.
When wanting to modify 1 image file or to delete a bulk of multiple images files, which were previously owned by an app, but lost ownership due to a re-installation, then what is the procedure to achieve such actions (delete or modify)?
The preferable solution would be not to use the SAF file picker, in the sense of avoid requesting to the user to select and grant a location through SAF.
And if the only solution is to use the SAF file picker, then how can be triggered to directly prompt to delete a set of known specific files without requesting tree access, neither having to tell the user to browse, search, and do it himself?
My final conclusions.
For APIs >= 29 is not possible to delete non-owned files without user interaction, and there is no way around this fact.
In Android 10/Q (API 29), a RecoverableSecurityException must be caught, then request the user permission, and finally if granted perform the deletion.
In Android 11/R (API 30) is greatly improved. Can delete in bulk even combining already owned files in the same batch. No need to handle anything after the request, the system takes care of the deletion if granted by the user.
The limitation is that it only handles media files (images, videos, audio). For other file types an IllegalArgumentException is thrown with the message: "All requested items must be referenced by specific ID", (check this message in MediaStore source code).
Note that in API 30 there is a new MANAGE_EXTERNAL_STORAGE permission, but its usage requires extra steps in the developer's console, such as to explain why the permission is needed.
Example:
public static void delete(final Activity activity, final Uri[] uriList, final int requestCode)
throws SecurityException, IntentSender.SendIntentException, IllegalArgumentException
{
final ContentResolver resolver = activity.getContentResolver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
{
// WARNING: if the URI isn't a MediaStore Uri and specifically
// only for media files (images, videos, audio) then the request
// will throw an IllegalArgumentException, with the message:
// 'All requested items must be referenced by specific ID'
// No need to handle 'onActivityResult' callback, when the system returns
// from the user permission prompt the files will be already deleted.
// Multiple 'owned' and 'not-owned' files can be combined in the
// same batch request. The system will automatically delete them
// using the same prompt dialog, making the experience homogeneous.
final List<Uri> list = new ArrayList<>();
Collections.addAll(list, uriList);
final PendingIntent pendingIntent = MediaStore.createDeleteRequest(resolver, list);
activity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, null);
}
else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q)
{
try
{
// In Android == Q a RecoverableSecurityException is thrown for not-owned.
// For a batch request the deletion will stop at the failed not-owned
// file, so you may want to restrict deletion in Android Q to only
// 1 file at a time, to make the experience less ugly.
// Fortunately this gets solved in Android R.
for (final Uri uri : uriList)
{
resolver.delete(uri, null, null);
}
}
catch (RecoverableSecurityException ex)
{
final IntentSender intent = ex.getUserAction()
.getActionIntent()
.getIntentSender();
// IMPORTANT: still need to perform the actual deletion
// as usual, so again getContentResolver().delete(...),
// in your 'onActivityResult' callback, as in Android Q
// all this extra code is necessary 'only' to get the permission,
// as the system doesn't perform any actual deletion at all.
// The onActivityResult doesn't have the target Uri, so you
// need to cache it somewhere.
activity.startIntentSenderForResult(intent, requestCode, null, 0, 0, 0, null);
}
}
else
{
// As usual for older APIs
for (final Uri uri : uriList)
{
resolver.delete(uri, null, null);
}
}
}
what is the procedure to achieve such actions (delete or modify)?
AFAIK, your only option is to use the SAF and get rights that way.
The preferable solution would be not to use the SAF file picker, in the sense of avoid requesting to the user to select and grant a location through SAF.
That's not possible. It would be a security flaw if it were. Please understand that while you think that these are your files, from the OS' standpoint, they are just files on the device. If apps could get arbitrary modification access to arbitrary files, that would be a step backwards from the fairly insecure stuff we had previously.
how can be triggered to directly prompt to delete a set of known specific files
There is no delete-document or delete-tree UI option in SAF, though it's not a bad idea.
neither having to tell the user to browse, search, and do it himself?
That you might be able to work around. You can try this:
Step #1: Get a Uri for one of the MediaStore entries (e.g., use ContentUris and one of the IDs from a query() for your content)
Step #2: Use getDocumentUri() to transmogrify that MediaStore Uri into an SAF Uri pointing to the same content
Step #3: Put that SAF Uri as the EXTRA_INITIAL_URI value in an ACTION_OPEN_DOCUMENT_TREE Intent, and use that to try to pre-populate the tree picker to your content's directory
Step #4: Validate that the Uri you get back from ACTION_OPEN_DOCUMENT_TREE is the one you are expecting (it has your files, it matches the EXTRA_INITIAL_URI, or something along those lines)
At this point, you now can delete the files using DocumentFile.fromTreeUri() to get a DocumentFile for the tree, and from there list the files in the tree and delete them.
Whether the Uri that you get from Step #2 will work for EXTRA_INITIAL_URI in Step #3 is unclear, as I haven't tried this yet (though it's on my to-do list for early next week...).
To delete a Single file from the Media Store do something like this, if the file is not part of your app then an intent will start to get permission
val uri: String? = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString()
val where = MediaStore.Audio.Media._ID + "=?"
val selectionArgs = arrayOf(mId)
try {
val deleted = mActivity.contentResolver.delete(Uri.parse(uri), where, selectionArgs)
return deleted >= 0
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw SecurityException()
val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
intentSender?.let {
mActivity.startIntentSenderForResult(intentSender, 0, null, 0, 0, 0, null)
}
} else {
throw SecurityException()
}
}
To add to the Media Store do something like this...
val values = ContentValues().apply {
put(MediaStore.Audio.Media.TITLE, song?.title)
put(MediaStore.MediaColumns.DISPLAY_NAME, song?.title)
put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.Audio.Media.MIME_TYPE, song?.mimeType)
}
val resolver = mContext.contentResolver
val uri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)
// Download file to Media Store
uri?.let { mUri ->
resolver.openOutputStream(mUri).use { mOutputStream ->
mOutputStream?.let {
// Download to output stream using the url we just created
}
}
}
Background
My app (here) can search for APK files throughout the file system (not just of installed apps), showing information about each, allowing to delete, share, install...
As part of the scoped-storage feature on Android Q, Google announced that SAF (storage access framework) will replace the normal storage permissions. This means that even if you will try to use storage permissions, it will only grant to access to specific types of files for File and file-path to be used or completely be sandboxed (written about here).
This means that a lot of frameworks will need to rely on SAF instead of File and file-path.
The problem
One of them is packageManager.getPackageArchiveInfo , which given a file path, returns PackageInfo , which I can get various information about:
name (on the current configuration) , AKA "label", using packageInfo.applicationInfo.loadLabel(packageManager) . This is based on the current configuration of the device (locale, etc...)
package name , using packageInfo.packageName
version code , using packageInfo.versionCode or packageInfo.longVersionCode .
version number , using packageInfo.versionName
app icon, using various ways, based on the current configuration (density etc... ) :
a. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)
b. if installed, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )
c. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)
There are a lot more that it returns you and a lot that are optional, but I think those are the basic details about APK files.
I hope Google will provide a good alternative for this (requested here and here ), because currently I can't find any good solution for it.
What I've tried
It's quite easy to use the Uri that I get from SAF and have an InputStream from it :
#TargetApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
packageInstaller = packageManager.packageInstaller
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/vnd.android.package-archive"
startActivityForResult(intent, 1)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && requestCode == 1 && resultCode == Activity.RESULT_OK && resultData != null) {
val uri = resultData.data
val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
if (!isDocumentUri)
return
val documentFile = DocumentFile.fromSingleUri(this, uri)
val inputStream = contentResolver.openInputStream(uri)
//TODO do something with what you got above, to parse it as APK file
But now you are stuck because all the framework I've seen needs a File or file-path.
I've tried to find any kind of alternative using the Android framework but I couldn't find any. Not only that, but all libraries I've found don't offer such a thing either.
EDIT: found out that one of the libraries I've looked at (here) - kinda has the option to parse APK file (including its resources) using just a stream, but :
The original code uses a file path (class is ApkFile), and it takes about x10 times more than normal parsing using the Android framework. The reason is probably that it parses everything possible, or close to it. Another way (class is ByteArrayApkFile ) to parse is by using a byte-array that includes the entire APK content. Very wasteful to read the entire file if you need just a small part of it. Plus it might take a lot of memory this way, and as I've tested, indeed it can reach OOM because I have to put the entire APK content into a byte array.
I've found out it sometimes fails to parse APK files that the framework can parse fine (here). Maybe it will soon be fixed.
I tried to extract just the basic parsing of the APK file, and it worked, but it's even worse in terms of speed (here). Took the code from one of the classes (called AbstractApkFile). So out of the file, I get just the manifest file which shouldn't take much memory, and I parse it alone using the library. Here:
AsyncTask.execute {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val apkFilePath = packageInfo.applicationInfo.publicSourceDir
// I'm using the path only because it's easier this way, but in reality I will have a Uri or inputStream as the input, which is why I use FileInputStream to mimic it.
val zipInputStream = ZipInputStream(FileInputStream(apkFilePath))
while (true) {
val zipEntry = zipInputStream.nextEntry ?: break
if (zipEntry.name.contains("AndroidManifest.xml")) {
Log.d("AppLog", "zipEntry:$zipEntry ${zipEntry.size}")
val bytes = zipInputStream.readBytes()
val xmlTranslator = XmlTranslator()
val resourceTable = ResourceTable()
val locale = Locale.getDefault()
val apkTranslator = ApkMetaTranslator(resourceTable, locale)
val xmlStreamer = CompositeXmlStreamer(xmlTranslator, apkTranslator)
val buffer = ByteBuffer.wrap(bytes)
val binaryXmlParser = BinaryXmlParser(buffer, resourceTable)
binaryXmlParser.locale = locale
binaryXmlParser.xmlStreamer = xmlStreamer
binaryXmlParser.parse()
val apkMeta = apkTranslator.getApkMeta();
Log.d("AppLog", "apkMeta:$apkMeta")
break
}
}
}
So, for now, this is not a good solution, because of how slow it is, and because getting the app name and icon requires me to give the entire APK data, which could lead to OOM. That's unless maybe there is a way to optimize the library's code...
The questions
How can I get an APK information (at least the things I've mentioned in the list) out of an InputStream of an APK file?
If there is no alternative on the normal framework, where can I find such a thing that will allow it? Is there any popular library that offers it for Android?
Note: Of course I could copy the InputStream to a file and then use it, but this is very inefficient as I will have to do it for every file that I find, and I waste space and time in doing so because the files already exist.
EDIT: after finding the workaround (here) to get very basic information about the APK via getPackageArchiveInfo (on "/proc/self/fd/" + fileDescriptor.fd) , I still can't find any way to get app-label and app-icon. Please, if anyone knows how to get those with SAW alone (no storage permission), let me know.
I've set a new bounty about this, hoping someone will find some workaround for this as well.
I'm putting a new bounty because of a new discovery I've found: An app called "Solid Explorer" targets API 29, and yet using SAF it can still show APK information, including app name and icon.
That's even though in the beginning when it first targeted API 29, it didn't show any information about APK files, including the icon and the app name.
Trying out an app called "Addons detector", I couldn't find any special library that this app uses for this purpose, which means it might be possible to do it using the normal framework, without very special tricks.
EDIT: about "Solid Explorer", seems that they just use the special flag of "requestLegacyExternalStorage", so they don't use SAF alone, but the normal framework instead.
So please, if anyone knows how to get app-name and app-icon using SAF alone (and can show it in a working sample), please let me know.
Edit: seems that the APK-parser library can get the app name fine and the icons, but for icons it has a few issues:
the qualifiers are a bit wrong, and you need to find which is the best for your case.
For adaptive icon it can get a PNG instead of VectorDrawable.
For VectorDrawable, it gets just the byte-array. No idea how to convert it to a real VectorDrawable.
OK I think I found a way using the Android framework (someone on reddit gave me this solution), to use file-path and use it, but it's not perfect at all. Some notes:
Not as direct as before.
Good thing is that it might also be possible to handle even files that are outside of the device storage.
It looks like a workaround, and I'm not sure for how long it will work.
For some reason, I can't load the app label (it always returns just the package name instead), and same goes for the app-icon (always null or default icon).
The solution, in short, is using this:
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
I think this same approach can be used for all cases that you need a file-path.
Here's a sample app (also available here) :
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/vnd.android.package-archive"), 1
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
try {
val uri = data?.data ?: return
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
val isDocumentUri = DocumentFile.isDocumentUri(this, uri)
if (!isDocumentUri)
return
val documentFile = DocumentFile.fromSingleUri(this, uri) ?: return
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
Log.d("AppLog", "got APK info?${packageArchiveInfo != null}")
if (packageArchiveInfo != null) {
val appLabel = loadAppLabel(packageArchiveInfo.applicationInfo, packageManager)
Log.d("AppLog", "appLabel:$appLabel")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("AppLog", "failed to get app info: $e")
}
}
fun loadAppLabel(applicationInfo: ApplicationInfo, packageManager: PackageManager): String =
try {
applicationInfo.loadLabel(packageManager).toString()
} catch (e: java.lang.Exception) {
""
}
}
}
Use below code
/**
* Get the apk path of this application.
*
* #param context any context (e.g. an Activity or a Service)
* #return full apk file path, or null if an exception happened (it should not happen)
*/
public static String getApkName(Context context) {
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
String apk = ai.publicSourceDir;
return apk;
} catch (Throwable x) {
return null;
}
}