android apk inputstream android-package-managers scoped-storage

android - ¿Cómo obtener información de un archivo APK en el sistema de archivos(no solo los instalados) sin usar File o file-path?



inputstream android-package-managers (2)

OK, creo que encontré una manera de usar el marco de Android (alguien en reddit me dio esta solución), para usar la ruta de archivo y usarla, pero no es perfecta en absoluto. Algunas notas:

  1. No es tan directo como antes.
  2. Lo bueno es que también podría ser posible manejar incluso archivos que están fuera del almacenamiento del dispositivo.
  3. Parece una solución alternativa, y no estoy seguro de cuánto tiempo funcionará.
  4. Por alguna razón, no puedo cargar la etiqueta de la aplicación (en su lugar, siempre devuelve solo el nombre del paquete), y lo mismo ocurre con el ícono de la aplicación (siempre nulo).

La solución, en resumen, es usar esto:

val fileDescriptor = contentResolver.openFileDescriptor(uri, "r") ?: return val packageArchiveInfo = packageManager.getPackageArchiveInfo("/proc/self/fd/" + fileDescriptor.fd, 0)

Creo que este mismo enfoque se puede utilizar para todos los casos en que necesite una ruta de archivo.

Aquí hay una aplicación de muestra (también disponible 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) { "" } } }

Antecedentes

Mi aplicación ( here ) puede buscar archivos APK en todo el sistema de archivos (no solo de las aplicaciones instaladas), mostrando información sobre cada una, permitiendo eliminar, compartir, instalar ...

Como parte de la función de almacenamiento con alcance en Android Q, Google anunció que SAF (marco de acceso de almacenamiento) reemplazará los permisos de almacenamiento normales. Esto significa que incluso si intenta usar los permisos de almacenamiento, solo otorgará acceso a tipos específicos de archivos para que File y file-path se usen o estén completamente protegidos (escrito here ).

Esto significa que muchos frameworks necesitarán confiar en SAF en lugar de File y file-path.

El problema

Uno de ellos es packageManager.getPackageArchiveInfo , que dada una ruta de archivo, devuelve PackageInfo , del que puedo obtener información variada sobre:

  1. nombre (en la configuración actual), AKA "etiqueta", usando packageInfo.applicationInfo.loadLabel(packageManager) . Esto se basa en la configuración actual del dispositivo (configuración regional, etc.)
  2. nombre del paquete, usando packageInfo.packageName
  3. código de versión, usando packageInfo.versionCode o packageInfo.longVersionCode .
  4. número de versión, usando packageInfo.versionName
  5. ícono de la aplicación, de varias formas, en función de la configuración actual (densidad, etc.):

a. BitmapFactory.decodeResource(packageManager.getResourcesForApplication(applicationInfo),packageInfo.applicationInfo.icon, bitmapOptions)

si. si está instalado, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )

C. ResourcesCompat.getDrawable(packageManager.getResourcesForApplication(applicationInfo), packageInfo.applicationInfo.icon, null)

Hay muchos más que te devuelven y muchos que son opcionales, pero creo que esos son los detalles básicos sobre los archivos APK.

Espero que Google proporcione una buena alternativa para esto (solicitada here y here ), porque actualmente no puedo encontrar ninguna buena solución para ello.

Lo que he intentado

Es bastante fácil usar el Uri que obtengo de SAF y tengo un InputStream de él:

@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

Pero ahora está atascado porque todo el marco que he visto necesita un archivo o ruta de archivo.

Intenté encontrar cualquier tipo de alternativa usando el marco de Android, pero no pude encontrar ninguna. No solo eso, sino que todas las bibliotecas que he encontrado tampoco ofrecen tal cosa.

EDITAR: descubrí que una de las bibliotecas que he visto ( here ) tiene un poco la opción de analizar el archivo APK (incluidos sus recursos) usando solo una secuencia, pero:

  1. El código original usa una ruta de archivo (la clase es ApkFile ), y toma aproximadamente 10 veces más que el análisis normal usando el marco de Android. Probablemente, la razón es que analiza todo lo posible o cerca de él. Otra forma (la clase es ByteArrayApkFile ) de analizar es mediante el uso de una matriz de bytes que incluye todo el contenido del APK. Es un desperdicio leer todo el archivo si solo necesita una pequeña parte de él. Además, puede tomar mucha memoria de esta manera, y como lo he probado, de hecho, puede llegar a OOM porque tengo que poner todo el contenido del APK en una matriz de bytes.

  2. Descubrí que a veces no se pueden analizar los archivos APK que el marco puede analizar bien ( here ). Quizás pronto se arregle.

  3. Traté de extraer solo el análisis básico del archivo APK, y funcionó, pero es aún peor en términos de velocidad ( here ). Tomó el código de una de las clases (llamado AbstractApkFile ). Entonces, fuera del archivo, obtengo solo el archivo de manifiesto que no debería ocupar mucha memoria, y lo analizo solo usando la biblioteca. Aquí:

    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 } } }

Entonces, por ahora, esta no es una buena solución, debido a lo lento que es y porque obtener el nombre y el ícono de la aplicación requiere que brinde todos los datos del APK, lo que podría conducir a OOM. Eso a menos que tal vez haya una manera de optimizar el código de la biblioteca ...

Las preguntas

  1. ¿Cómo puedo obtener una información de APK (al menos las cosas que he mencionado en la lista) de un InputStream de un archivo APK?

  2. Si no hay alternativa en el marco normal, ¿dónde puedo encontrar algo que lo permita? ¿Hay alguna biblioteca popular que la ofrezca para Android?

Nota: Por supuesto, podría copiar el InputStream a un archivo y luego usarlo, pero esto es muy ineficiente ya que tendré que hacerlo para cada archivo que encuentre, y pierdo espacio y tiempo al hacerlo porque los archivos ya existen .

EDITAR: después de encontrar la solución ( here ) para obtener información muy básica sobre el APK a través de getPackageArchiveInfo (en "/proc/self/fd/" + fileDescriptor.fd ), todavía no puedo encontrar ninguna manera de obtener la etiqueta de la aplicación y icono de la aplicación Por favor, si alguien sabe cómo obtener esos con SAW solo (sin permiso de almacenamiento), hágamelo saber.

He establecido una nueva recompensa sobre esto, con la esperanza de que alguien encuentre alguna solución para esto también.

Estoy poniendo una nueva recompensa debido a un nuevo descubrimiento que he encontrado: una aplicación llamada " Solid Explorer " se dirige a API 29, y aun así, usando SAF, todavía puede mostrar información de APK, incluido el nombre y el icono de la aplicación.

Eso es a pesar de que al principio cuando se dirigió por primera vez a API 29, no mostraba ninguna información sobre los archivos APK, incluido el icono y el nombre de la aplicación.

Al probar una aplicación llamada " Detector de complementos ", no pude encontrar ninguna biblioteca especial que esta aplicación use para este propósito, lo que significa que es posible hacerlo usando el marco normal, sin trucos muy especiales.

EDITAR: sobre "Solid Explorer", parece que solo usan el indicador especial de "requestLegacyExternalStorage", por lo que no usan SAF solo, sino el marco normal en su lugar.

Entonces, por favor, si alguien sabe cómo obtener el nombre de la aplicación y el ícono de la aplicación usando SAF solo (y puede mostrarlo en una muestra funcional), hágamelo saber.


Use el siguiente código

/** * 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; } }