Как получить информацию об APK-файле без использования File или file-path?
Фон
Мое приложение ( здесь) может сканировать файлы APK во всей файловой системе, показывая информацию о каждом из них, позволяя удалять, делиться, устанавливать...
В рамках функции хранения с ограниченным объемом в Android Q компания Google объявила, что SAF (структура доступа к хранилищу) заменит обычные разрешения для хранилища. Это означает, что даже если вы попытаетесь использовать разрешения для хранилища, он предоставит доступ только к определенным типам файлов для файла и пути к файлу, которые будут использоваться или полностью помещаться в "песочницу" (об этом написано здесь).
Это означает, что многие фреймворки должны будут полагаться на SAF, а не на файл и путь к файлу.
Эта проблема
Одним из них является packageManager.getPackageArchiveInfo, который, учитывая путь к файлу, возвращает PackageInfo, о котором я могу получить различную информацию о:
- имя (в текущей конфигурации), AKA "метка", используя
packageInfo.applicationInfo.loadLabel(packageManager)
- имя пакета, используя
packageInfo.packageName
- код версии, используя
packageInfo.versionCode
или жеpackageInfo.longVersionCode
, - номер версии, используя
packageInfo.versionName
- Значок приложения, используя различные способы, в текущей конфигурации:
а. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)
б. если установлен, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )
с. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)
Что я пробовал
Довольно просто использовать Uri, полученный из SAF, и получить из него 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 inputStream = contentResolver.openInputStream(uri)
Но теперь вы застряли, потому что все, что я видел, требует файла или пути к файлу.
Я пытался найти какую-либо альтернативу, используя платформу Android, но не смог ее найти. Не только это, но и все библиотеки, которые я обнаружил, не предлагают ничего подобного.
Вопросы
Как я могу получить информацию об APK (по крайней мере, то, что я упомянул в списке) из InputStream файла APK?
Если нет альтернативы нормальному фреймворку, где я могу найти такую вещь, которая позволит это? Есть ли какая-нибудь популярная библиотека, которая предлагает его для Android?
Примечание: Конечно, я мог бы скопировать InputStream в файл и затем использовать его, но это очень неэффективно, так как мне придется делать это для каждого файла, который я нахожу, и я трачу на это место и время, потому что файлы уже существовать.
1 ответ
Хорошо, я думаю, что нашел способ использовать фреймворк 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) {
""
}
}
}
Как вы упомянули,getPackageArchiveInfo(String archiveFilePath, int flags)
извлекает PackageInfo
используя archiveFilePath
но вы хотите другой подход.
Решение
Вы можете получить все packagesNames
установлен в устройстве с помощью следующего намерения, а затем методом getPackageInfo(String packageName, int flags)
легко получить доступ к его информации.(Это как-то ошибка безопасности и конфиденциальности!)
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> pkgAppsList=context.getPackageManager().queryIntentActivities(mainIntent,0);
for(int i = 0 ; i< pkgAppsList.size() ;i++)
{
PackageInfo info = context.getPackageManager().getPackageInfo(pkgAppsList.get(i).packageName,1);
// TODO
// do what you need for each package .
}
Используйте код ниже
/**
* 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;
}
}