How to get information of an APK file in the file system (not just installed ones) without using File or file-path?
Asked Answered
R

2

52

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:

  1. name (on the current configuration) , AKA "label", using packageInfo.applicationInfo.loadLabel(packageManager) . This is based on the current configuration of the device (locale, etc...)
  2. package name , using packageInfo.packageName
  3. version code , using packageInfo.versionCode or packageInfo.longVersionCode .
  4. version number , using packageInfo.versionName
  5. 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 :

  1. 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.

  2. I've found out it sometimes fails to parse APK files that the framework can parse fine (here). Maybe it will soon be fixed.

  3. 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

  1. 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?

  2. 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:

  1. the qualifiers are a bit wrong, and you need to find which is the best for your case.
  2. For adaptive icon it can get a PNG instead of VectorDrawable.
  3. For VectorDrawable, it gets just the byte-array. No idea how to convert it to a real VectorDrawable.
Rau answered 25/5, 2019 at 22:18 Comment(20)
"How can I get an APK information... out of an InputStream of an APK file?" -- copy the bytes to a file that you control, then use getPackageArchiveInfo() on that file. Yes, I know, this is slow and space-consuming, but it is far and away your simplest and most reliable option. Otherwise, you'd need to rummage through the AOSP code, find the stuff that implements getPackageArchiveInfo(), and try creating your own clone of it. I'll be fairly surprised if anyone has done that already.Mostly
As I wrote, the question is without using File or file-path. Your solution uses just that. As for people who did what you wrote, there was one actually, but it's not for Android, and I don't think it has InputStream as parameter and not sure if it supports getting the icon. I think it's here: github.com/lxbzmy/android-apk-parserRau
Have you checked this? #20068008 I think you can get Path from URI, and continue with your older logic. @androiddeveloperChew
@NaitikSoni I've already tried this trick. As I said, I can't use storage permission anymore because it is planned to be gone in the future, and be replaced by permissions that allow to reach only a very specific types of files (media files) or be sandboxed completely. So, because of this, as I wrote, I need a solution that will use SAF alone. No other permissions, especially not storage permission which is planned to be gone.Rau
What about unzipping the apk files in memory? It seems that APK file is a ZIP archive in fact. This looks like a good starting point: mkyong.com/java/how-to-decompress-files-from-a-zip-fileKenwee
Right. But you shouldn't unzip it all because that would be wasteful (takes time and storage even though you need to handle around 2 fils: manifest and icon). Just what's needed, and for that you need to know how to parse its content. BTW, you can't use ZipFile class (developer.android.com/reference/java/util/zip/ZipFile) because it requires a file path, but you can use ZipInputStream (developer.android.com/reference/java/util/zip/ZipInputStream ) instead.Rau
Actually it might be a bit more than 2 files, because it needs the app name too, and just like the app icon, it's based on the current configuration (locale, density, etc...)Rau
If we have a value for uri on onActivityResult does that mean that uri.toFile().path is also accessible ?Priddy
@ManojPerumarath Sadly I've tested it already: Even if you find out the path, and you have access to it via SAF, you don't really have access to it via File API or file-path. I've reported about this here: issuetracker.google.com/issues/132481545 . Also requested to be able to do as such here: issuetracker.google.com/issues/134473341 . Sadly Google seems to go for it to the end, breaking so many apps and libraries, and it doesn't even performs well at all either: issuetracker.google.com/issues/130261278 . :(Rau
Find installed packages with packageManager.getInstalledPackages(0). Then for each returned package use getPackageInfo(packageName,flags) to get PackageInfo. For APKs not returned in this list or can't be coerced to return in this list with the flags, you may need to copy them locally to get the information but that may be a much smaller list. What am I missing?Unwinking
@Unwinking I've specifically wrote APK file (even in the title). Not installed app. The sample I wrote is only to access it easily (the APK file is always there). I even wrote a comment about it inside the code. Copying to a new file means you don't open the file, but you open another file, which is a waste in time and space (imagine doing it for a hundred files of various sizes). I already wrote about this too. Please read the entire post. The question, again, is how to get ANY APK file (from storage) information without using file-path or File API.Rau
I mean, there is even an answer of another user who asked the same thing, and I answered him that this is not my question. It's not about installed apps only. It's about all APK files that the device has on its storage.Rau
Google seems so obsessed with security that they are making it impossible to write a useful app. Have you thought of using c++ code and bypass the nonsense, and rely on the underlying UNIX-like permissions of the file system/s?Erlin
@JonGoodwin I hope I won't have to go this path. Was a very long time since I tried using JNI for Android.Rau
I'd wonder if Storage Access Framework even is the right tool for scanning the file-system. If trying to fit into the framework, this would have to be some kind of ApkMetaContentProvider. The mere question is, if paths will be completely abstracted away in future API levels... accessing the SD card is no problem with C++/JNI; that just would need an array of file-systems to traverse.Endocrinology
@MartinZeitler Wait you have a new idea of a solution for this? Have you checked it?Rau
@androiddeveloper so far I have at least one SAF file-picker, passing a user selection to JNI for the processing. the paths just look a little different, so it would be most easy to somehow obtain a list first of all available locations, because those paths are not as abstracted as in Java (assuming to look for APK files on MMC & SD). On a notebook right now, which is of no use for Android Studio ...Endocrinology
@MartinZeitler OK if you have any progress for Android, including app-name and app-icon (including even adaptive-icons), please let me knowRau
@MartinZeitler going C++ will not help you here, Android 29+ guards file access through the underlying Unix file system.Farrago
@AlexCohn It generally seems as if a whole lot of freedom and possibilities are being replaced with so called "security"... and this can at times become obsessive. I'd guess that these file-systems already had octal permissions and SE contexts before already, but these may have been adjusted to be only accessible from within a certain context.Endocrinology
R
6

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:

  1. Not as direct as before.
  2. Good thing is that it might also be possible to handle even files that are outside of the device storage.
  3. It looks like a workaround, and I'm not sure for how long it will work.
  4. 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) {
                ""
            }
    }
}
Rau answered 9/8, 2019 at 6:54 Comment(8)
Reading files through the /proc/self/fd/… symlink may fail on Android 29+. OTOH, requestLegacyExternalStorage flag is a legitimate way to work with file-path-based API.Farrago
requestLegacyExternalStorage is not possible when targeting API 30. Sadly, the current solution that I use seems to be archived : github.com/hsiafan/apk-parser . I have made a working fork of it, but I'm not a professional on this topic (parsing APK files) : github.com/AndroidDeveloperLB/apk-parser . The solution allows you to parse APK files even using an InputStreamRau
See developer.android.com/about/versions/11/privacy/… To help your app work more smoothly with third-party media libraries, Android 11 allows you to use APIs other than the MediaStore API to access media files from shared storage using direct file paths. These APIs include the following: – The File API. – Native libraries, such as fopen().Farrago
@AlexCohn That's not a media file. It's APK file, and in some cases it might even be in a ZIP file. Sadly the current API of Android can't even parse split APK files. That's even more reason to use a third party library. Sadly though, again, this is the only good library I've found to do it, and it got archived.Rau
Are you saying that this reversal to File API access on Android 11 is limited to mp4, mp3, etc files? Do they check by file name or by contents?Farrago
@AlexCohn You mentioned "MediaStore " so it's not related to APK files. The "READ_EXTERNAL_STORAGE " will be used only for media, sadly. That's the purpose of the flag you've mentioned, but it will work only on API 29 or when targeting it. For "true" access to all files, apps targeting Android API 30 will have to get the new permission ( MANAGE_EXTERNAL_STORAGE) , which Google said that to publish an app that uses it - you need to fill a form (which isn't published yet). I wrote it as "true" because sadly you can't reach some "Android/..." folders.Rau
True, your app_manager app needs MANAGE_EXTERNAL_STORAGE.Farrago
@AlexCohn They didn't publish the form yet, so no app can be on the Play Store and target API 30. I hope it won't cause even more issues...Rau
I
-4

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;
    }
}
Insignia answered 21/6, 2019 at 7:24 Comment(2)
Again, just as I wrote before to the other answer (here: https://mcmap.net/q/344269/-how-to-get-information-of-an-apk-file-in-the-file-system-not-just-installed-ones-without-using-file-or-file-path ) , in the comments , in the title and in the question itself : I'm not asking for getting information of installed apps . I'm asking about getting information of ANY APK file on the file-system. You don't have "packageName" as an input. You don't even have a file-path as an input. You have SAF API to be used instead, which provides you DocumentFile, a Uri, or an InputStream.Rau
Here, I've updated the question even further, to make sure people understand.Rau

© 2022 - 2024 — McMap. All rights reserved.