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.
One of them is packageManager.getPackageArchiveInfo , which given a file path, returns PackageInfo , which I can get various information about:
packageInfo.applicationInfo.loadLabel(packageManager)
. This is based on the current configuration of the device (locale, etc...)packageInfo.packageName
packageInfo.versionCode
or packageInfo.longVersionCode
. packageInfo.versionName
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.
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...
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:
May be the easy one to see the source: In Android studio 2.3, Build -> Analyze APK -> Select the apk that you want to decompile . You will see it's source code.
To bypass this download restriction and install APK files from unknown sources, navigate to one of these menus, depending on your Android version: Settings > Apps > Special app access > Install unknown apps. Settings > Apps & notifications > Advanced > Special app access > Install unknown apps.
apk? For normal apps, there are stored in internal memory in /data/app. Some of the encrypted apps, the files are stored in /data/app-private. For apps stored in the external memory, files are stored in /mnt/sdcard/Android/data.
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:
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) { "" } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With