Neulich hat sich bei einem meiner kleinen Freizeitprojekte das Problem ergeben, dass der in der Anwendung verwendete ContentProvider
auch größere Binärdaten (genauer Bilder) zur Verfügung stellen sollte. Ein Blick in die Android-SDK brachte auch tatsächlich etwas zu dem Thema zum Vorschein (aus dem Artikel zu ContentProvidern):
If you are exposing byte data that’s too big to put in the table itself — such as a large bitmap file — the field that exposes the data to clients should actually contain a
content:
URI string. This is the field that gives clients access to the data file. The record should also have another field, named “_data
” that lists the exact file path on the device for that file. This field is not intended to be read by the client, but by the ContentResolver. […]
Dieses Feature klang recht interessant, allerdings war mir die Erklärung doch etwas zu lang, um das gleich in das “richtige” Projekt einzubauen. Stattdessen habe ich mich an ein kleines Testprojekt gesetzt, um mir die Zusammenhänge etwas zu verdeutlichen. Damit Andere auch etwas davon haben, gibt es das Projekt jetzt als Open-Source auf github und hier ein paar erklärende Worte.
Wenn man das Beispielprojekt auf einem Android-Gerät startet, dann sieht man eine Liste deren Elemente jeweils aus einem Bild und einer Zahl bestehen. Des Datenumfangs wegen arbeitet das Projekt nicht mit vorher erstellten Bildern sondern mit Zufallsbildern, die mit einem bestimmten Startwert erstellt werden (die Zahl neben dem Bild). Die Aktivität, die diese Liste anzeigt (ImageListActivity
) beschränkt sich auf das Wesentliche und implementiert nur onCreate(Bundle)
, um die Daten vom ContentProvider zu erhalten.
Neben der Aktivität gibt es noch einen kleinen IntentService
(ImageCreateService
), der das eigentliche Erstellen der Bilder übernimmt (zusammen mit der RandomImage
Klasse), da dies zu Zeitaufwändig ist um im GUI-Thread durchgeführt zu werden.
Zentraler Punkt des Beispielprojektes ist natürlich der ContentProvider selbst, zu finden in der ImagesProvider
Klasse. Implementiert sind nur onCreate()
, getType(Uri), query(...)
und openFile(Uri, String)
, der ContentProvider ist also nur für Lesezugriffe geeignet. Die query(...)
Methode liefert einen MatrixCursor
zurück, der drei Spalten besitzt: “_id”, “image” und “_data”. Das ID-Feld enthält die zum Bild gehörige Zufallszahl, die zwei anderen Felder sind für die Auslieferung der Bilddaten zuständig.
Wie im SDK-Text nachzulesen war, enthält die “image” Spalte eine “content://“-URI, die in diesem Fall wieder zum gleichen ContentProvider zeigt, aber durchaus auch einen Anderen benutzen kann. In der “_data” Spalte ist wie in der SDK beschrieben ein Dateiname eingetragen, die von der openFile(Uri, String)
Methode zum Öffnen der Datei verwendet wird. Ein Query an den ContentProvider liefert nun beispielsweise folgendes Ergebnis (gekürzt):
_id | image | _data
10 | content://net.sourcewalker.binary/images/10 | /mnt/sdcard/.../files/10.png
[ Zeilen ausgelassen ]
800 | content://net.sourcewalker.binary/images/800 | /mnt/sdcard/.../files/800.png
Allerdings wird dem Aufrufer beim Abruf der “image” Spalte nicht die vom ContentProvider erzeugte URI zurückgeliefert. Stattdessen wird diese automatisch vom ContentResolver
aufgelöst und die openFile(...)
des passenden ContentProviders (in diesem Fall der Gleiche) mit der URI als Argument aufgerufen. Diese Methode liefert dann einen Dateideskriptor (ParcelFileDescriptor
) zurück, der benutzt werden kann um die Binärdaten zu lesen. Die ContentProvider-Klasse stellt dazu eine Hilfsmethode ContentProvider.openFileHelper(Uri, String)
zur Verfügung. Diese Methode führt ein Query auf der entsprechenden URI aus und erwartet eine Spalte “_data” mit einem Dateinamen für den dann der Dateideskriptor zurückgeliefert wird.
Die Erzeugung der Bilddateien findet in diesem Beispielprojekt im ImageCreateService
statt, der mittels eines Intent gestartet wird sobald die Bilderliste vom ContentProvider abgerufen wird. Während der Service die Bilddatei erstellt wird dem Datenempfänger ein “Wartebild” gezeigt, das in diesem Fall einfach leer ist. Der Service benachrichtigt alle fünf Bilder mittels ContentResolver.notifyChange(...)
die Aktivität, worauf die erzeugten Bilder an die Aktivität geliefert werden.
So, jetzt müssen die neuen Erkenntnisse nur noch in das richtige Projekt integriert werden und alles ist in Ordnung. Ich hoffe der Artikel ist hilfreich. Bei Fragen oder Anmerkungen bitte ich um Kommentar oder Email, insbesondere wenn Fehler gefunden werden.
Gruß
Xp
Update: Ich habe das Projekt und diesen Artikel umgeschrieben, so dass die “empfohlene” Implementierung mittels ContentProvider.openFileHelper
verwendet wird. Der alte Stand des Quellcodes ist immernoch im Branch “own-openfile” im github-Projekt enthalten.