Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
338 views
in Technique[技术] by (71.8m points)

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

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.
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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) {
                ""
            }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...