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
orpackageInfo.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 isByteArrayApkFile
) 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.
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).
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;
}
}
来源:https://stackoverflow.com/questions/56309165/how-to-get-information-of-an-apk-file-in-the-file-system-not-just-installed-one