android - Wie kann ich Informationen zu einer APK-Datei im Dateisystem(nicht nur den installierten) abrufen, ohne Datei oder Dateipfad zu verwenden?




inputstream android-package-managers (2)

OK, ich glaube, ich habe einen Weg gefunden, das Android-Framework zu verwenden (jemand auf reddit hat mir diese Lösung gegeben), um den Dateipfad zu verwenden und es zu verwenden, aber es ist überhaupt nicht perfekt. Einige Notizen:

  1. Nicht so direkt wie zuvor.
  2. Gut ist, dass es auch möglich sein kann, Dateien zu verarbeiten, die sich außerhalb des Gerätespeichers befinden.
  3. Es sieht nach einer Problemumgehung aus, und ich bin mir nicht sicher, wie lange es funktionieren wird.
  4. Aus irgendeinem Grund kann ich das App-Label nicht laden (es gibt stattdessen immer nur den Paketnamen zurück), und das Gleiche gilt für das App-Icon (immer null).

Kurz gesagt, die Lösung lautet:

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

Ich denke, dass der gleiche Ansatz für alle Fälle verwendet werden kann, in denen Sie einen Dateipfad benötigen.

Hier ist eine Beispiel-App (auch here erhältlich):

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) {
                ""
            }
    }
}

Hintergrund

Meine App ( here ) kann im gesamten Dateisystem (nicht nur bei installierten Apps) nach APK-Dateien suchen, die Informationen zu den einzelnen Apps anzeigen und das Löschen, Freigeben, Installieren usw. ermöglichen.

Als Teil der Scoped-Storage-Funktion unter Android Q gab Google bekannt, dass SAF (Storage Access Framework) die normalen Speicherberechtigungen ersetzen wird. Dies bedeutet, dass selbst wenn Sie versuchen, Speicherberechtigungen zu verwenden, nur der Zugriff auf bestimmte Dateitypen gewährt wird, damit Datei und Dateipfad verwendet oder vollständig in einer Sandbox gespeichert werden können ( here ).

Dies bedeutet, dass viele Frameworks auf SAF anstatt auf File und File-Path angewiesen sind.

Das Problem

Eines davon ist packageManager.getPackageArchiveInfo , das bei Angabe eines Dateipfads PackageInfo , über das ich verschiedene Informationen erhalten kann:

  1. name (in der aktuellen Konfiguration), AKA "label" mit packageInfo.applicationInfo.loadLabel(packageManager) . Dies basiert auf der aktuellen Konfiguration des Geräts (Gebietsschema usw.).
  2. Paketname mit packageInfo.packageName
  3. Versionscode mit packageInfo.versionCode oder packageInfo.longVersionCode .
  4. Versionsnummer mit packageInfo.versionName
  5. App-Symbol auf verschiedene Arten, basierend auf der aktuellen Konfiguration (Dichte usw.):

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

b. falls installiert, AppCompatResources.getDrawable(createPackageContext(packageInfo.packageName, 0), packageInfo.applicationInfo.icon )

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

Es gibt noch viel mehr, was Sie zurückgibt, und vieles, was optional ist, aber ich denke, das sind die grundlegenden Details zu APK-Dateien.

Ich hoffe, Google bietet hierfür eine gute Alternative ( here und here angefordert), da ich derzeit keine gute Lösung dafür finden kann.

Was ich versucht habe

Es ist ziemlich einfach, den Uri zu verwenden, den ich von SAF bekomme und einen InputStream von ihm zu haben:

@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

Aber jetzt stecken Sie fest, weil jedes Framework, das ich gesehen habe, eine Datei oder einen Dateipfad benötigt.

Ich habe versucht, mithilfe des Android-Frameworks eine Alternative zu finden, konnte aber keine finden. Nicht nur das, sondern auch alle Bibliotheken, die ich gefunden habe, bieten so etwas nicht an.

BEARBEITEN: habe herausgefunden, dass eine der Bibliotheken, die ich mir angesehen habe ( here ) - irgendwie hat die Option, APK-Datei (einschließlich ihrer Ressourcen) nur mit einem Stream zu analysieren, aber:

  1. Der ursprüngliche Code verwendet einen Dateipfad (Klasse ist ApkFile ) und es dauert ungefähr 10-mal mehr als normales Parsen unter Verwendung des Android-Frameworks. Der Grund ist wahrscheinlich, dass es alles Mögliche analysiert, oder in der Nähe davon. Eine andere Möglichkeit (Klasse ist ByteArrayApkFile ) zum Parsen ist die Verwendung eines Byte-Arrays, das den gesamten APK-Inhalt enthält. Es ist sehr verschwenderisch, die gesamte Datei zu lesen, wenn Sie nur einen kleinen Teil davon benötigen. Außerdem kann es auf diese Weise viel Speicher beanspruchen, und wie ich getestet habe, kann es tatsächlich OOM erreichen, da ich den gesamten APK-Inhalt in ein Byte-Array packen muss.

  2. Ich habe herausgefunden, dass es manchmal nicht gelingt, APK-Dateien zu analysieren, die das Framework gut analysieren kann ( here ). Vielleicht wird es bald behoben.

  3. Ich habe versucht, nur das grundlegende Parsen der APK-Datei zu extrahieren, und es hat funktioniert, aber es ist noch schlimmer in Bezug auf die Geschwindigkeit ( here ). Hat den Code von einer der Klassen ( AbstractApkFile ) übernommen. Aus der Datei erhalte ich also nur die Manifest-Datei, die nicht viel Speicherplatz beanspruchen sollte, und analysiere sie allein mithilfe der Bibliothek. Hier:

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

Im Moment ist dies keine gute Lösung, da sie sehr langsam ist und ich zum Abrufen des App-Namens und -Symbols die gesamten APK-Daten eingeben muss, was zu OOM führen kann. Es sei denn, es gibt vielleicht eine Möglichkeit, den Code der Bibliothek zu optimieren ...

Die Fragen

  1. Wie kann ich aus einem InputStream einer APK-Datei APK-Informationen abrufen (zumindest die in der Liste genannten Informationen)?

  2. Wenn es im normalen Framework keine Alternative gibt, wo finde ich dann eine Alternative? Gibt es eine beliebte Bibliothek, die es für Android anbietet?

Hinweis: Natürlich könnte ich den InputStream in eine Datei kopieren und dann verwenden, aber das ist sehr ineffizient, da ich es für jede Datei tun muss, die ich finde, und ich verschwende dabei Platz und Zeit, da die Dateien bereits vorhanden sind .

BEARBEITEN: Nachdem ich die Problemumgehung ( here ) gefunden habe, um grundlegende Informationen über die APK über getPackageArchiveInfo (unter "/proc/self/fd/" + fileDescriptor.fd ) zu erhalten, kann ich immer noch keine Möglichkeit finden, App-Label und zu erhalten app-icon . Bitte, wenn jemand weiß, wie er diese mit SAW alleine bekommt (keine Speichererlaubnis), lass es mich wissen.

Ich habe ein neues Kopfgeld dafür ausgesetzt, in der Hoffnung, dass jemand auch dafür eine Lösung findet.

Ich setze ein neues Kopfgeld ein, weil ich eine neue Entdeckung gefunden habe: Eine App mit dem Namen " Solid Explorer " zielt auf API 29 ab und kann mit SAF dennoch APK-Informationen anzeigen, einschließlich App-Name und Symbol.

Dies ist auch dann der Fall, wenn am Anfang, als API 29 zum ersten Mal aufgerufen wurde, keine Informationen zu APK-Dateien angezeigt wurden, einschließlich Symbol und Name der App.

Als ich eine App namens " Addons Detector " ausprobierte , konnte ich keine spezielle Bibliothek finden, die diese App für diesen Zweck verwendet, was bedeutet, dass es möglich sein könnte, dies mit dem normalen Framework ohne sehr spezielle Tricks zu tun.

BEARBEITEN: Bei "Solid Explorer" wird anscheinend nur das spezielle Flag "requestLegacyExternalStorage" verwendet, sodass nicht nur SAF, sondern stattdessen das normale Framework verwendet wird.

Also bitte, wenn jemand weiß, wie man App-Namen und App-Symbole mit SAF alleine bekommt (und es in einem funktionierenden Beispiel zeigen kann), lass es mich wissen.


Verwenden Sie den folgenden 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;
    }
}




scoped-storage