android - Come ottenere informazioni su un file APK nel file system(non solo quelli installati) senza utilizzare File o percorso file?




inputstream android-package-managers (2)

OK, penso di aver trovato il modo di usare il framework Android (qualcuno su reddit mi ha dato questa soluzione), per usare il percorso del file e usarlo, ma non è affatto perfetto. Alcune note:

  1. Non diretto come prima.
  2. La cosa buona è che potrebbe anche essere possibile gestire anche i file che si trovano al di fuori della memoria del dispositivo.
  3. Sembra una soluzione alternativa e non sono sicuro per quanto tempo funzionerà.
  4. Per qualche motivo, non riesco a caricare l'etichetta dell'app (restituisce sempre solo il nome del pacchetto), e lo stesso vale per l'icona dell'app (sempre nulla).

La soluzione, in breve, sta usando questo:

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

Penso che questo stesso approccio possa essere utilizzato per tutti i casi in cui è necessario un percorso file.

Ecco un'app di esempio (disponibile anche 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) {
                ""
            }
    }
}

sfondo

La mia app ( here ) può cercare file APK in tutto il file system (non solo delle app installate), mostrando informazioni su ciascuna di esse, consentendo di eliminare, condividere, installare ...

Come parte della funzionalità di archiviazione con ambito su Android Q, Google ha annunciato che SAF (framework di accesso all'archiviazione) sostituirà le normali autorizzazioni di archiviazione. Ciò significa che anche se proverai a utilizzare le autorizzazioni di archiviazione, consentirà solo di accedere a tipi specifici di file per File e percorso file da utilizzare o essere completamente sandbox (scritto here ).

Ciò significa che molti framework dovranno fare affidamento su SAF anziché su File e percorso del file.

Il problema

Uno di questi è packageManager.getPackageArchiveInfo , che ha fornito un percorso di file, restituisce PackageInfo , che posso ottenere varie informazioni su:

  1. nome (nella configurazione corrente), AKA "etichetta", usando packageInfo.applicationInfo.loadLabel(packageManager) . Questo si basa sulla configurazione corrente del dispositivo (impostazioni internazionali, ecc ...)
  2. nome del pacchetto, usando packageInfo.packageName
  3. codice versione, usando packageInfo.versionCode o packageInfo.longVersionCode .
  4. numero di versione, usando packageInfo.versionName
  5. icona dell'app, usando vari modi, in base alla configurazione corrente (densità ecc ...):

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

b. se installato, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )

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

Ci sono molte altre cose che ti restituiscono e molte che sono opzionali, ma penso che questi siano i dettagli di base sui file APK.

Spero che Google fornirà una buona alternativa per questo (richiesto here e here ), perché al momento non riesco a trovare una buona soluzione per questo.

Quello che ho provato

È abbastanza facile usare l'Uri che ottengo da SAF e avere un InputStream da esso:

@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

Ma ora sei bloccato perché tutto il framework che ho visto ha bisogno di un file o percorso di file.

Ho provato a trovare qualsiasi tipo di alternativa usando il framework Android ma non sono riuscito a trovarne. Non solo, ma tutte le biblioteche che ho trovato non offrono nemmeno una cosa del genere.

EDIT: ho scoperto che una delle librerie che ho visto ( here ) - kinda ha l'opzione per analizzare il file APK (comprese le sue risorse) usando solo uno stream, ma:

  1. Il codice originale utilizza un percorso di file (la classe è ApkFile ) e impiega circa 10 volte più del normale analisi utilizzando il framework Android. Il motivo è probabilmente che analizza tutto il possibile o vicino ad esso. Un altro modo (la classe è ByteArrayApkFile ) di analizzare consiste nell'utilizzare un array di byte che include l'intero contenuto APK. Molto dispendioso leggere l'intero file se ne serve solo una piccola parte. Inoltre potrebbe richiedere molta memoria in questo modo e, come ho testato, in effetti può raggiungere OOM perché devo mettere l'intero contenuto APK in un array di byte.

  2. Ho scoperto che a volte non riesce ad analizzare i file APK che il framework può analizzare bene ( here ). Forse sarà presto risolto.

  3. Ho provato a estrarre solo l'analisi di base del file APK, e ha funzionato, ma è anche peggio in termini di velocità ( here ). Ha preso il codice da una delle classi (chiamato AbstractApkFile ). Quindi dal file ottengo solo il file manifest che non dovrebbe occupare molta memoria e lo analizzo da solo usando la libreria. Qui:

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

Quindi, per ora, questa non è una buona soluzione, a causa di quanto è lento e perché ottenere il nome e l'icona dell'app mi richiede di fornire tutti i dati APK, il che potrebbe portare a OOM. Questo a meno che forse non ci sia un modo per ottimizzare il codice della biblioteca ...

Le domande

  1. Come posso ottenere informazioni APK (almeno le cose che ho menzionato nell'elenco) da un InputStream di un file APK?

  2. Se non esiste un'alternativa al framework normale, dove posso trovare una cosa che lo consenta? Esiste una libreria popolare che la offre per Android?

Nota: ovviamente potrei copiare InputStream in un file e quindi utilizzarlo, ma questo è molto inefficiente in quanto dovrò farlo per ogni file che trovo, e perdo tempo e spazio a perdere perché i file esistono già .

EDIT: dopo aver trovato la soluzione ( here ) per ottenere informazioni di base getPackageArchiveInfo tramite getPackageArchiveInfo (su "/proc/self/fd/" + fileDescriptor.fd ), non riesco ancora a trovare un modo per ottenere l' etichetta app e icona-app . Per favore, se qualcuno sa come ottenere quelli con SAW da solo (nessun permesso di archiviazione), fammi sapere.

Ho creato una nuova taglia su questo, sperando che qualcuno trovi qualche soluzione anche per questo.

Sto mettendo una nuova taglia a causa di una nuova scoperta che ho trovato: un'app chiamata " Solid Explorer " prende di mira l'API 29, e tuttavia usando SAF può ancora mostrare informazioni APK, incluso il nome e l'icona dell'app.

Questo anche se all'inizio, quando ha preso di mira l'API 29, non ha mostrato alcuna informazione sui file APK, inclusi l'icona e il nome dell'app.

Provando un'app chiamata " Addons detector ", non sono riuscito a trovare alcuna libreria speciale che questa app utilizza a questo scopo, il che significa che potrebbe essere possibile farlo utilizzando il normale framework, senza trucchi molto speciali.

EDIT: su "Solid Explorer", sembra che utilizzino solo il flag speciale di "requestLegacyExternalStorage", quindi non usano solo SAF, ma invece il normale framework.

Quindi, per favore, se qualcuno sa come ottenere il nome e l'icona dell'app usando solo SAF (e può mostrarlo in un campione funzionante), per favore fatemelo sapere.


Usa sotto il codice

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




scoped-storage