hsiafan / apk-parser

Apk parser for java
BSD 2-Clause "Simplified" License
1.22k stars 360 forks source link

Request: allow to parse APK file without using File or file-path #92

Open AndroidDeveloperLB opened 5 years ago

AndroidDeveloperLB commented 5 years ago

Use InputStream , DocumentFile or Uri instead.

The reason: In the near future, Google will block storage permission, which requires developers to handle files outside of the scope of the current app, to use SAF instead of File and file-path. More information here: https://issuetracker.google.com/issues/128591846

And yes, it's very devastating for many apps and libraries that need to access the files in a normal way. Even the Android framework isn't ready for it, as there are functions such as getPackageArchiveInfo (here) that uses a File or file-path, and have no other alternative.

If you want to test your solution without the storage permission and using just the InputStream, here (the app assumes there is a file named "a.apk" on the root path of the normal storage) : https://issuetracker.google.com/issues/132481545#comment5

hsiafan commented 5 years ago

Does ByteArrayApkFile meet your need?

AndroidDeveloperLB commented 5 years ago

You mean this: https://github.com/hsiafan/apk-parser/blob/master/src/main/java/net/dongliu/apk/parser/ByteArrayApkFile.java

This means it has to load the entire APK content in order to parse it. I don't think that's how the Android parser works. Imagine I have to parse multiple files to show them in a list. If I had to load each into memory entirely it would load much slower than loading only what's needed. It will also be a waste for memory, and might cause OOM in case the APK is large. I think it directly goes to the parts that are important: the manifest and later, upon demand, the drawables and the strings (using current configuration, including locale, density, Android version, etc...) .

AndroidDeveloperLB commented 5 years ago

Via the SAF API , I could give you the InputStream multiple times if needed.

AndroidDeveloperLB commented 5 years ago

Maybe it's possible to have a ZipInputStream that will take only the manifest file and send it as a byte array to ApkFile? Will the library still be able to parse a byte array that only has the manifest file? The only missing thing in this idea is what to do with app-icons. Maybe will need to parse the manifest file, check which image file is needed, and then use the ZipInputStream again to get it? But what if it's an adaptiveIcon ... So many possible issues...

AndroidDeveloperLB commented 5 years ago

I tried now to get only the manifest into bytes array. Seems to work, but the parser of this library can't handle it alone. It probably has to have it in an APK file, together with some other stuff.

Here's the code :

        AsyncTask.execute {
            val packageInfo = packageManager.getPackageInfo(packageName, 0)
            val apkFilePath = packageInfo.applicationInfo.publicSourceDir
            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 apkFile=ByteArrayApkFile(bytes)
                    val apkMeta = apkFile.apkMeta
                    Log.d("AppLog", "apkMeta : $apkMeta ")
                }
            }
        }
hsiafan commented 5 years ago

Resource table file is a must to get real values of AndroidManifest.xml. Maybe we can provide a minimal api which only read Resource table and AndroidManifest.xml.

AndroidDeveloperLB commented 5 years ago

I see. Makes sense. But I think it shouldn't read everything that it can. Just what it needs. Does the library first parse the manifest, and then check on the table what is needed, and takes it? Or does it load all possible resources first, and then check what the manifest needs?

AndroidDeveloperLB commented 4 years ago

OK seems it's almost possible to do it, for manifest and resources files. All I have to do is to load each of those into byte-array and then parse. I wonder though if those files can be too large to handle.

            val manifestBytes: ByteArray? = get bytes array from AndroidConstants.MANIFEST_FILE entry
            val resourcesBytes: ByteArray? =get bytes array from AndroidConstants.RESOURCE_FILE entry
            val xmlTranslator = XmlTranslator()
            val resourceTable: ResourceTable =
                    if (resourcesBytes == null)
                        ResourceTable()
                    else {
                        val resourceTableParser = ResourceTableParser(ByteBuffer.wrap(resourcesBytes))
                        resourceTableParser.parse()
                        resourceTableParser.resourceTable
                    }
            val apkMetaTranslator = ApkMetaTranslator(resourceTable, locale)
            val binaryXmlParser = BinaryXmlParser(ByteBuffer.wrap(manifestBytes), resourceTable)
            binaryXmlParser.locale = locale
            binaryXmlParser.xmlStreamer = CompositeXmlStreamer(xmlTranslator, apkMetaTranslator)
            binaryXmlParser.parse()