问题描述
背景
我的应用程序( 此处 )可以在整个文件系统(不仅是已安装的应用程序)中搜索APK文件,显示有关每个文件的信息,从而可以删除,共享,安装...
作为Android Q范围存储功能的一部分,Google宣布SAF(存储访问框架)将取代常规存储权限.这意味着,即使您尝试使用存储权限,也只会授予访问特定类型的文件的权限,以使文件和文件路径可以使用或完全沙盒化(写为 此处 ).
这意味着很多框架将需要依赖SAF而不是File和file-path.
问题
其中之一是给出文件路径的 packageManager.getPackageArchiveInfo 返回 PackageInfo ,我可以获得有关以下信息的各种信息:
- 名称(在当前配置上),使用
packageInfo.applicationInfo.loadLabel(packageManager)
,也称为标签".这基于设备的当前配置(语言环境等) - 软件包名称,使用
packageInfo.packageName
- 版本代码,使用
packageInfo.versionCode
或packageInfo.longVersionCode
. - 版本号,使用
packageInfo.versionName
- 应用图标,根据当前配置(密度等...)以各种方式使用:
a. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)
b.如果已安装,则AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )
c. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)
它还可以返回您很多,还有很多是可选的,但我认为这些是有关APK文件的基本信息.
我希望Google为此提供一个很好的选择(要求此处和 此处 ),因为目前我找不到任何好的解决方案.
我尝试过的
使用从SAF获得的Uri并从中获得InputStream很容易:
@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
但是现在您被困住了,因为我见过的所有框架都需要一个File或file-path.
我尝试使用Android框架找到任何替代方案,但找不到任何替代方案.不仅如此,我发现的所有库也不提供这种功能.
发现我看过其中一个库( 此处 )-可以选择仅使用流来解析APK文件(包括其资源),但是:
-
原始代码使用文件路径(类为
ApkFile
),它所花费的时间比使用Android框架进行的正常解析大约多10倍.原因可能是它解析了所有可能或接近的一切.解析的另一种方法(类为ByteArrayApkFile
)是通过使用包含整个APK内容的字节数组.如果只需要读取一小部分文件,则非常浪费时间.另外,这种方式可能会占用大量内存,而且正如我测试过的那样,确实可以达到OOM,因为我必须将整个APK内容放入字节数组中. -
我发现有时无法解析框架可以很好解析的APK文件( 此处 ).也许很快就会解决.
-
我试图仅提取APK文件的基本解析,并且可以正常工作,但是在速度方面更糟糕( 此处 ).从其中一个类(称为
AbstractApkFile
)中获取了代码.因此,从文件中,我得到的清单文件应该不占用太多内存,我只使用库来解析它.在这里: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 } } }
因此,就目前而言,这不是一个好的解决方案,因为它的速度很慢,并且因为获取应用程序名称和图标需要我提供整个APK数据,这可能会导致OOM.除非有办法优化库的代码,否则……
问题
-
如何从APK文件的InputStream中获取APK信息(至少是我在列表中提到的内容)?
-
如果常规框架上没有其他选择,我在哪里可以找到允许的选择?有没有流行的库可以为Android提供它?
注意:当然,我可以将InputStream复制到文件中,然后使用它,但这效率很低,因为我必须为找到的每个文件执行该操作,并且这样做会浪费时间和时间,因为文件已经存在.
找到解决方法后( 此处 ),以获得有关以下内容的非常基本的信息:通过getPackageArchiveInfo
(位于"/proc/self/fd/" + fileDescriptor.fd
上)的APK,我仍然找不到任何方法来获取应用标签和应用图标.请,如果有人知道如何单独使用SAW(没有存储权限),请告诉我.
我为此设置了新的赏金,希望有人也能为此找到一些解决方法.
由于发现了一个新发现,我要提供新的赏金:一个名为" 固体资源管理器 "以API 29为目标,但使用SAF仍可以显示APK信息,包括应用程序名称和图标.
即使在最初首次针对API 29时,它并没有显示有关APK文件的任何信息,包括图标和应用名称.
尝试一个名为" 插件检测器 ",我找不到该应用程序用于此目的的任何特殊库,这意味着可以使用常规框架进行操作,而无需非常特殊的技巧./p>
关于"Solid Explorer",似乎他们只是使用"requestLegacyExternalStorage"的特殊标志,因此他们不单独使用SAF,而是使用常规框架.
因此,如果有人知道如何仅使用SAF来获取应用名称和应用图标(并可以在工作示例中显示),请告诉我.
似乎 APK解析器库可以使应用名称变好,并且图标,但是对于图标有一些问题:
- 限定词为有点错误,您需要找到最适合您的情况.
- 对于自适应图标,它可以获取PNG 而不是VectorDrawable.
- 对于VectorDrawable,它的只是字节数组.不知道如何将其转换为真实的VectorDrawable.
好,我想我找到了一种使用Android框架的方法(reddit上的某个人给了我这种解决方案),使用文件路径并使用它,但事实并非如此.完美无缺.一些注意事项:
- 不像以前那样直接.
- 好消息是,甚至可以处理设备存储之外的文件.
- 这似乎是一种解决方法,我不确定它能工作多长时间.
- 由于某种原因,我无法加载应用标签(它始终仅返回包名称),并且应用图标也是如此(始终为null或默认图标).
简而言之,该解决方案正在使用此方法:
val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return
val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)
我认为在需要文件路径的所有情况下都可以使用相同的方法.
这是一个示例应用程序(也可以在> 此处 ):
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) {
""
}
}
}
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.
Edit: seems that the APK-parser library can get the app name fine and the icons, but for icons it has a few issues:
- the qualifiers are a bit wrong, and you need to find which is the best for your case.
- For adaptive icon it can get a PNG instead of VectorDrawable.
- For VectorDrawable, it gets just the byte-array. No idea how to convert it to a real VectorDrawable.
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 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) {
""
}
}
}
这篇关于如何在不使用文件或文件路径的情况下获取文件系统(不仅仅是已安装的文件)中APK文件的信息?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!