Eine Bundesligatabelle als Android Content Provider – Teil 1: Ablage der Daten

1. Aufgabe

In dieser und den folgenden beiden Aufgaben widmen wir uns dem Themenbereich eines Content Providers unter Android. Content Provider stellen eine Abstraktionsschicht für den Zugriff auf Daten dar, gewissermaßen eine Entkopplung der Anwendungsebene von der Datenschicht. Die Daten können bei diesem Konzept aus Sicht des Verbrauchers von einer anderen App desselben Smartphones – einem anderen Prozess des gemeinsamen Android Betriebssystems – bereit gestellt werden oder sogar auf einem anderen Rechner liegen, der über eine Netzwerkverbindung erreichbar ist. Für den Zugriff selbst hat man sich an der Vorgehensweise durch SQL und dem damit verbundenen Modell eines relationalen Datenbanksystems orientiert. Die Daten des Content Providers liegen auf dem Zielsystem häufig in einer Datenbank, es könnten aber auch andere Software-Datenstrukturen und -Konzepte zum Einsatz kommen (Dateien, XML, Arrays, etc.) .

Einige Content Provider sind in Android standardmäßig vorhanden, wie zum Beispiel der Kalender, das Kontakteverzeichnis, das Telefon und die Medienbibliothek. In dieser Fallstudie, die sich insgesamt über drei Aufgaben erstreckt, beschäftigen wir uns – versprochen: zum letzten Mal – mit der Verwaltung einer Tabelle für einen Ligaspielbetrieb. Um den verteilten Ansatz der Content Provider Architektur besser hervorheben zu können, teilen wir das Thema auf drei Anwendungen auf:

  • Anwendung League Data Provider: Bereitstellung des Datenbestands.

  • Anwendung League Data Admin: Administration des Datenbestands.

  • Anwendung League Data Viewer: Browsing des Datenbestands.

Die erste App – der eigentliche „Content Provider“ – stellt den Content nach den Richtlinien des Android Betriebssystems zur Verfügung. Zum einen bedeutet dies, dass alle (lesenden und schreibenden) Zugriffe in diesem Prozess auflaufen. Des weiteren sind bestimmte Zeichenketten, die so genannten Autoritäten, zu definieren, um auf einzelne Teilbereiche des Datenbestands zugreifen zu können. Für das Füllen des Datenbestands zeichnet sich eine zweite App verantwortlich, ich habe sie „League Data Admin“ genannt. Sie besitzt insbesondere die Erlaubnis des schreibenden Zugriffs auf den Datenbestand. Diese App sollte im Gegensatz zur „League Data Viewer“-App nur von einigen wenigen Personen benutzt werden, eben den Administratoren des Datenbestands.

Am meisten spielt bei einem Ligaspielbetrieb natürlich die aktuelle Ligatabelle eine Rolle. Ihre Darstellung obliegt der App „League Data Viewer“. Um die Zuständigkeiten nicht durcheinander zu bringen: Das Berechnen der aktuellen Ligatabelle erfolgt natürlich im Kontext der „League Data Provider“ App. Durch entsprechende Anfragen kann dieser Vorgang (Berechnung der aktuellen Ligatabelle) angestoßen werden und eine Übertragung des Ergebnisses erfolgen. Die „League Data Viewer“-App selbst besitzt nur lesende Zugriffsrechte auf den Datenhaushalt des Providers. Es ist mit ihrer Hilfe nicht möglich, Mannschaften oder Spielresultate im Datenhaushalt einzutragen. Für diesen Zweck gibt es ja die „League Data Admin“-App. Natürlich hätte man all diese Funktionalitäten in einer einzigen App vereinen können. Um den App-übergreifenden Zugriff des Content Provider-Konzepts besser hervorzuheben zu können, habe ich die lesenden und schreibenden Zugriffe auf mehrere verschiedene Anwendungen aufgeteilt.

Damit wenden wir uns der ersten App dieser Trilogie zu: Erstellen Sie eine Android App, die für den Ligaspielbetrieb einer populären Sportart die teilnehmenden Mannschaften und die Spielresultate verwaltet und eine Ligatabelle berechnet. Die Funktionalität soll im Rahmen des Android Content Provider Konzepts erfolgen. Bzgl. der Bedienoberfläche gibt es keine besonderen Anforderungen, da der (lesende und schreibende) Zugriff auf den Datenbestand durch zwei andere Apps erfolgt.

Eine minimale Oberfläche der einzigen App-Aktivität könnte wie in Abbildung 1 gezeigt aussehen. Dabei bietet es sich an, die Mengengerüste der einzelnen SQLite-Tabellen oder andere system-relevante Informationen zur Anzeige zu bringen.

Minimalistische Oberfläche der League Data Provider App.

Abbildung 1. Minimalistische Oberfläche der „League Data Provider“ App.

2. Lösung

Fangen wir mit der Implementierung eines benutzerdefinierten Android Content Providers an. Folgende Schritte müssen zu diesem Zweck betrachtet werden:

  1. Definition einer benutzerdefinierten Klasse, die die Klasse ContentProvider spezialisiert.

  2. Festlegung der Autoritäts-Zeichenketten, der Uri-Objekte sowie entsprechender UriMatcher Definitionen.

  3. Implementierung der geerbten abstrakten Methoden der Klasse ContentProvider. Im Einzelnen sind dies

    • die Methode onCreate,

    • die Methode getType sowie

    • die vier CRUD-Methoden insert (entspricht CREATE), query (entspricht READ), update (UPDATE) und delete (DELETE).

  4. Erweiterung der Datei AndroidManifest.xml um entsprechende Content Provider Informationen.

In der Regel erfolgt die Implementierung durch mindestens zwei (oder mehreren) Klassen. Getrennt von der eigentlichen Realisierung der Datenzugriffsfunktionen ist der Kontrakt des Content Providers. Im Folgenden stellen wir somit zwei Klassen LeagueDataContract (Listing 1) und LeagueDataProvider (Listing 2) vor, auf die einzelnen Aspekte in der Entwicklung eines Android Content Providers gehen wir unmittelbar im Anschluss danach ein:

01: package com.example.leaguedataprovider;
02: 
03: import android.provider.BaseColumns;
04: 
05: public abstract class LeagueDataContract {
06:     
07:     /**
08:      *  version
09:      */
10:     public static final String LEAGUE_PROVIDER_VERSION = "0.26";
11:     
12:     /**
13:      *  authority of the league data content provider
14:      */
15:     public static final String LEAGUE_PROVIDER_AUTHORITY =
16:         "com.example.LeagueDataProvider";
17:     
18:     /**
19:      *  content string for the top-level league data authority 
20:      */
21:     public static final String LEAGUE_PROVIDER_CONTENT =
22:         "content://" + LEAGUE_PROVIDER_AUTHORITY;
23:     
24:     /**
25:      *  data paths for specific contents of the league data content provider
26:      */
27:     public static final String PATH_TEAMS = "teams";
28:     public static final String PATH_GAMES = "games";
29:     public static final String PATH_TABLE = "table";    
30:     
31:     /**
32:      *  content strings for "teams", "games" and "table" content
33:      */
34:     public static final String CONTENT_TEAMS =
35:         LEAGUE_PROVIDER_CONTENT + "/" + PATH_TEAMS;
36:     public static final String CONTENT_GAMES =
37:         LEAGUE_PROVIDER_CONTENT + "/" + PATH_GAMES;
38:     public static final String CONTENT_TABLE =
39:         LEAGUE_PROVIDER_CONTENT + "/" + PATH_TABLE;
40: 
41:     public static abstract class Teams implements BaseColumns {
42: 
43:         // column names for table "teams"
44:         // (no distinction made between virtual and real names)
45:         public static final String NAME = "NAME";
46:         public static final String CITY = "CITY";     
47:     }
48: 
49:     public static abstract class Games implements BaseColumns {
50:         
51:         // column names for table "games"
52:         // (no distinction made between virtual and real names)
53:         public static final String MATCHDAY  = "MATCHDAY";
54:         public static final String TEAMHOME  = "TEAMHOME";
55:         public static final String TEAMAWAY  = "TEAMAWAY";
56:         public static final String GOALSHOME = "GOALSHOME";
57:         public static final String GOALSAWAY = "GOALSAWAY";     
58:     }
59:     
60:     public static abstract class Table implements BaseColumns {
61:         
62:         // column names for league table "table" (virtual table)
63:         public static final String MATCHDAY    = "MATCHDAY";    
64:         public static final String TEAMNAME    = "TEAMNAME";
65:         public static final String GAMESPLAYED = "GAMESPLAYED";
66:         public static final String WINS        = "WINS";
67:         public static final String DRAWS       = "DRAWS";
68:         public static final String LOSSES      = "LOSSES";
69:         public static final String GSELF       = "GSELF";
70:         public static final String GOTHER      = "GOTHER";
71:         public static final String GDIFF       = "GDIFF";
72:         public static final String POINTS      = "POINTS";    
73:     }
74: }

Beispiel 1. Klasse LeagueDataContract: Kontrakt des Content Providers.


Definitionen der Autoritäts-Zeichenketten und der Uri-Objekte

Wir beginnen zunächst mit der Betrachtung von Listing 1. Um einen bestimmten Content Provider durch eine Android App anzusprechen, muss man seine URI kennen, bzw. aus Entwicklersicht gesehen, diese definieren. Der strukturelle Aufbau einer Content Provider URI sieht so aus:

content://<authority>/<path>

Details zu diesem Aufbau beschreiben wir in Tabelle 1:

Element

Beschreibung

Präfix

Das Präfix ist fest vorgegeben und muss immer content:// lauten.

Authority

Mit der Autorität wird der wichtigste Teil des Content Provider Namens festgelegt, wie die Bezeichnung bereits zum Ausdruck bringt. Für die standardmäßig vorhanden Android Provider sind diese Namen eher kurz gehalten wie zum Beispiel contacts, telephony usw. Um für die Bezeichner von third-party Content Providern eine weltweite Namenseindeutigkeit zu erzielen, sollte es sich um voll-qualifizierte Bezeichner wie etwa com.mycompany.statusprovider oder Vergleichbares handeln, die man prinzipiell auch zur Bezeichnung von Ressourcen im WWW verwenden könnte. Auf diese Weise kann es niemals eintreten, dass zwei unterschiedliche Entwickler denselben Namen für ihren Content Provider festlegen.

Pfad

Mit dem Pfad-Anteil der URI wird eine bestimmte Information (in der Regel: Datenbanktabelle) in der Datenhaltung des Providers angesprochen. Beispiel: Um den Standard-Android Content Provider für Mediadateien und hier im Speziellen die Tabelle aller Interpreten anzusprechen, gibt es die URI content://MediaStore.Audio.Artists.

Id

Wie beim Zugriff auf eine bestimmte Reihe einer SQL-Tabelle kann man diese auch über die Indirektionsstufe eines Content Provider adressieren. Zu diesem Zweck ist an den Pfad noch eine Id anzuhängen, zum Beispiel content://MediaStore.Audio.Artists/5.

Tabelle 1. Aufbau einer Content Provider URI.


Wenn Sie den Inhalt von Listing 1 genau betrachten, werden Sie in der Klasse LeagueDataContract nur die Definitionen von zahlreichen (unveränderbaren) Zeichenketten vorfinden. Warum fehlen die entsprechenden Uri-Objekte, so wie man sie beim Zugriff auf Content Provider benötigt und wie dies in vielen Aufsätzen in der einschlägigen Literatur und im Internet auch immer wieder vorgeschlagen wird? Der Grund dafür liegt zunächst einmal darin, dass ich – ebenfalls für eine vorbildliche Modellimplementierung – die Apps für Bereitstellung und Benutzung der Daten voneinander getrennt habe. Des Weiteren habe ich die öffentlichen Definitionen der LeagueDataContract-Klasse ebenfalls gemäß den Regeln eines sauberen Software-Entwurfs in einer Java-Klassenbibliothek ausgelagert, die von Clients für den Zugriff auf den Content Provider zu importieren ist (dazu mehr in der zweiten Folge dieser Fallstudie).

Und damit sind wir leider in den Niederungen der App-Programmierung unter Android angekommen, die machmal auch sehr unangenehme Seiten haben kann. Um es noch einmal hervorzuheben: Wir reden im Augenblick von Variablendefinitionen der Gestalt

public static final Uri CONTENT_URI = Uri.parse("content://com.example.LeagueDataProvider");  
public static final Uri CONTENT_URI_TEAMS = Uri.withAppendedPath (CONTENT_URI, "teams");

so wie man sie häufig in den Musterimplementierungen von Content Provider Kontraktklassen vorfindet. Unter den zuvor beschriebenen Randbedingungen (Auslagerung der Kontraktklasse in eine Java-Klassenbibliothek) kommt es zu folgendem Phänomen: Sowohl Provider- wie auch Client-Implementierung lassen sich (in der Eclipse-Entwicklungsumgebung) fehlerfrei übersetzen! Zur Ausführungszeit der Client-App kommt es aber zu Laufzeitausnahmen des Typs java.lang.ClassNotFoundException. Aus mir unerklärlichen Gründen weist der Klassen-Lader des Emulators (oder auch der eines realen Devices) ein anderes Laufzeitverhalten als der des Java-Compilers zur Übersetzungszeit auf. Die Suche nach diesem Fehler hat mich Stunden gekostet, zumindest ist mir nach eingehender Analyse gelungen, im Internet Leidensgenossen dieser Problematik aufzuspüren: „Previously in eclipse, marking a project to depend on another lead to the compiled .class files from the dependency to be included in your classes.dex and .apk file. This is no longer the case. We encountered the same problem while trying to create a 'content provider'. If you follow the Android guidelines and create a static CONTENT_URI as recommended you will compile in eclipse (if you are using dependent projects) but break at runtime, as the class verifier on the device (or emulator) will not have access to the content provider class. The 'solution' that we used was to not refer to the content providers 'CONTENT_URI' and simply inlined the URI into the code that made the content resolver calls.

Zumindest ist es mit dieser Erkenntnis möglich, das Fehlerverhalten auf eine pragmatische Weise zu lösen.

Noch ein weiterer Hinweis zu Listing 1. Neben den Festlegungen der Autoritäts-Zeichenketten finden Sie in der Klasse LeagueDataContract auch die Definitionen von Spalten für (SQLite-)Tabellen vor. Prinzipiell ist es sowohl möglich als auch erwünscht, zwischen den tatsächlichen Spaltennamen von SQLite-Datenbanktabellen und den (virtuellen) Spaltennamen für den Zugriff auf Datenprovider-Inhalte einen Unterschied zu machen. Um die Komplexität der vorgestellten Modellimplementierung nicht noch weiter zu steigern, habe ich für beide Anforderungen dieselben Namen gewählt und deshalb in der Kontraktklasse öffentlich deklariert.

Mit diesem Wissen gestärkt konnte ich nun alle Ratschläge zum sauberen Design eines Android Content Providers zu Gunsten einer etwas einfacher gestrickten Lösung ohne schlechtes Gewissen über Bord werfen. Wir sind in Listing 2 bei der Realisierung des Content Providers angekommen:

001: package com.example.leaguedataprovider;
002: 
003: import android.content.ContentProvider;
004: import android.content.ContentResolver;
005: import android.content.ContentUris;
006: import android.content.ContentValues;
007: import android.content.Context;
008: import android.content.UriMatcher;
009: import android.database.Cursor;
010: import android.database.SQLException;
011: import android.database.sqlite.SQLiteDatabase;
012: import android.database.sqlite.SQLiteOpenHelper;
013: import android.database.sqlite.SQLiteQueryBuilder;
014: import android.net.Uri;
015: import android.text.TextUtils;
016: 
017: public class LeagueDataProvider extends ContentProvider {
018:     
019:     /**
020:      *  helper constants for use with the UriMatcher object
021:      */
022:     private static final int TEAMS_LIST = 1;  
023:     private static final int TEAMS_ID = 2;  
024:     private static final int GAMES_LIST = 3;  
025:     private static final int GAMES_ID = 4;  
026:     private static final int TEAMS_LIST_COUNT = 5;
027:     private static final int GAMES_LIST_COUNT = 6;
028:     private static final int LEAGUE_TABLE = 7; 
029:  
030:     private static final UriMatcher uriMatcher;  
031:       
032:     /**
033:       *  database specific declarations
034:       */
035:     private static final String DATABASE_NAME = "LeagueDatabase.db";
036:     private static final String SQLITE_TABLE_TEAMS = "TEAMS";
037:     private static final String SQLITE_TABLE_GAMES = "GAMES";
038:     
039:     private static final String DATATABLE_TEAMS_CREATE =
040:         "CREATE TABLE IF NOT EXISTS " + SQLITE_TABLE_TEAMS + " (" +
041:         "_id INTEGER PRIMARY KEY NOT NULL," +
042:         "NAME TEXT NOT NULL," +
043:         "CITY TEXT NOT NULL);"; 
044:     
045:     private static final String DATATABLE_GAMES_CREATE =
046:         "CREATE TABLE IF NOT EXISTS " + SQLITE_TABLE_GAMES + " (" +
047:         "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
048:         "MATCHDAY INTEGER NOT NULL," +
049:         "TEAMHOME INTEGER NOT NULL," +
050:         "TEAMAWAY INTEGER NOT NULL," +
051:         "GOALSHOME INTEGER NOT NULL," +
052:         "GOALSAWAY INTEGER NOT NULL);"; 
053: 
054:     private static final String DATATABLE_TEAMS_DELETE =
055:         "DROP TABLE IF EXISTS " + SQLITE_TABLE_TEAMS;
056:     
057:     private static final String DATATABLE_GAMES_DELETE =
058:         "DROP TABLE IF EXISTS " + SQLITE_TABLE_GAMES;    
059:         
060:     private static final String DATATABLE_LEAGUETABLE =
061:         "SELECT " +
062:             "TEAMID as _id, " +   // SimpleCursorAdapter needs unique id named '_id'
063:             "TEAMS.NAME AS TEAMNAME, " +
064:             "COUNT(*) AS GAMESPLAYED, " +
065:             "SUM ( CASE " +
066:                 "WHEN GOALSSELF > GOALSOTHER THEN 1 " +
067:                 "ELSE 0 " +
068:             "END ) AS WINS, " +
069:             "SUM ( CASE " +
070:                 "WHEN GOALSSELF = GOALSOTHER THEN 1 " +
071:                 "ELSE 0 " +
072:             "END ) AS DRAWS, " +
073:             "SUM ( CASE " +
074:                 "WHEN GOALSSELF < GOALSOTHER THEN 1 " +
075:                 "ELSE 0 " +
076:             "END ) AS LOSSES, " +
077:             "SUM (GOALSSELF) AS GSELF, " +
078:             "SUM (GOALSOTHER) AS GOTHER, SUM (GOALSSELF - GOALSOTHER) AS GDIFF, " +
079:             "SUM ( CASE " +
080:                 "WHEN GOALSSELF > GOALSOTHER THEN 2 " +
081:                 "WHEN GOALSSELF = GOALSOTHER THEN 1 " +
082:                 "ELSE 0 " +
083:             "END ) AS POINTS " +
084:         "FROM (" +
085:             "SELECT " +
086:                 "GAMES.TEAMHOME AS TEAMID, GAMES.GOALSHOME AS GOALSSELF, " +
087:                 "GAMES.GOALSAWAY AS GOALSOTHER FROM GAMES " +
088:             " UNION ALL " +
089:             "SELECT " +
090:                 "GAMES.TEAMAWAY AS TEAMID, GAMES.GOALSAWAY AS GOALSSELF, " +
091:                 "GAMES.GOALSHOME AS GOALSOTHER FROM GAMES " +
092:         ") AS TABLEENTRIES " +
093:         "JOIN TEAMS ON TABLEENTRIES.TEAMID = TEAMS._id " +
094:         "GROUP BY TEAMID " +
095:         "ORDER BY POINTS DESC, GDIFF DESC, GSELF DESC";   
096:     
097:     private static final String DATATABLE_LEAGUETABLE_MATCHDAY =
098:         "SELECT " +
099:             "TEAMID as _id, " +   // SimpleCursorAdapter needs unique id named '_id'
100:             "TEAMS.NAME AS TEAMNAME, " +
101:             "COUNT(*) AS GAMESPLAYED, " +
102:             "SUM ( CASE " +
103:                 "WHEN GOALSSELF > GOALSOTHER THEN 1 " +
104:                 "ELSE 0 " +
105:             "END ) AS WINS, " +
106:             "SUM ( CASE " +
107:                 "WHEN GOALSSELF = GOALSOTHER THEN 1 " +
108:                 "ELSE 0 " +
109:             "END ) AS DRAWS, " +
110:             "SUM ( CASE " +
111:                 "WHEN GOALSSELF < GOALSOTHER THEN 1 " +
112:                 "ELSE 0 " +
113:             "END ) AS LOSSES, " +
114:             "SUM (GOALSSELF) AS GSELF, " +
115:             "SUM (GOALSOTHER) AS GOTHER, SUM (GOALSSELF - GOALSOTHER) AS GDIFF, " +
116:             "SUM ( CASE " +
117:                 "WHEN GOALSSELF > GOALSOTHER THEN 2 " +
118:                 "WHEN GOALSSELF = GOALSOTHER THEN 1 " +
119:                 "ELSE 0 " +
120:             "END ) AS POINTS " +
121:         "FROM (" +
122:             "SELECT " +
123:                 "GAMES.MATCHDAY AS MATCHDAY, " + 
124:                 "GAMES.TEAMHOME AS TEAMID, GAMES.GOALSHOME AS GOALSSELF, " +
125:                 "GAMES.GOALSAWAY AS GOALSOTHER FROM GAMES " +
126:             " UNION ALL " +
127:             "SELECT " +
128:                 "GAMES.MATCHDAY AS MATCHDAY, " + 
129:             "    GAMES.TEAMAWAY AS TEAMID, GAMES.GOALSAWAY AS GOALSSELF, " +
130:                 "GAMES.GOALSHOME AS GOALSOTHER FROM GAMES " +
131:         ") AS TABLEENTRIES " +
132:         "JOIN TEAMS ON TABLEENTRIES.TEAMID = TEAMS._id " +
133:         "WHERE MATCHDAY BETWEEN 1 AND %s " +
134:         "GROUP BY TEAMID " +
135:         "ORDER BY POINTS DESC, GDIFF DESC, GSELF DESC"; 
136:     
137:     /**
138:      *  SQLite specific objects
139:      */ 
140:     private static final int DATABASE_VERSION = 1;
141:     private DatabaseHelper dbHelper = null; 
142: 
143:     /**
144:      *  helper class that actually creates and manages 
145:      *  the provider's underlying data repository.
146:      */
147:     private class DatabaseHelper extends SQLiteOpenHelper {
148:         
149:         DatabaseHelper(Context context) {
150:             super(context, DATABASE_NAME, null, DATABASE_VERSION);
151:         }
152: 
153:         @Override
154:         public void onCreate(SQLiteDatabase db) {
155:             db.execSQL(DATATABLE_TEAMS_CREATE);
156:             db.execSQL(DATATABLE_GAMES_CREATE);
157:         }
158:         
159:         @Override
160:         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
161:             db.execSQL(DATATABLE_TEAMS_DELETE);
162:             db.execSQL(DATATABLE_GAMES_DELETE);
163:             this.onCreate(db);
164:         }
165:     }
166:     
167:     static {
168:         uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);        
169:         uriMatcher.addURI(
170:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
171:             LeagueDataContract.PATH_TEAMS,
172:             TEAMS_LIST);
173:         uriMatcher.addURI(
174:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
175:             LeagueDataContract.PATH_TEAMS + "/#",
176:             TEAMS_ID); 
177:         uriMatcher.addURI(
178:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
179:             LeagueDataContract.PATH_GAMES,
180:             GAMES_LIST);
181:         uriMatcher.addURI(
182:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
183:             LeagueDataContract.PATH_GAMES  + "/#",
184:             GAMES_ID);
185:         uriMatcher.addURI(
186:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
187:             LeagueDataContract.PATH_TEAMS + "/count",
188:             TEAMS_LIST_COUNT);
189:         uriMatcher.addURI(
190:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
191:             LeagueDataContract.PATH_GAMES + "/count",
192:             GAMES_LIST_COUNT);
193:         uriMatcher.addURI(
194:             LeagueDataContract.LEAGUE_PROVIDER_AUTHORITY,
195:             LeagueDataContract.PATH_TABLE,
196:             LEAGUE_TABLE);
197:     }
198:     
199:     public LeagueDataProvider ()
200:     {
201:         super();
202:         System.out.println ("c'tor LeagueDataProvider () ...");
203:     }
204: 
205:     @Override
206:     public boolean onCreate() {
207:         Context context = this.getContext();
208:         this.dbHelper = new DatabaseHelper(context);
209:         return true;
210:     }
211: 
212:     @Override
213:     public Cursor query (
214:             Uri uri, String[] projection, String selection,
215:             String[] selectionArgs, String sortOrder) {
216:         
217:         // returning records based on selections criteria
218:         SQLiteDatabase db = this.dbHelper.getReadableDatabase();
219:         if (db == null) {
220:             System.out.println ("LeagueDataProvider::getReadableDatabase failed !!!");
221:             return null;
222:         }        
223:         
224:         // just for better readability
225:         String groupBy = null;
226:         String having = null;
227:         
228:         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
229:         Cursor cursor = null;
230:         String rawSql;
231:         
232:         int match = LeagueDataProvider.uriMatcher.match(uri);
233:         switch (match) {
234:         case TEAMS_LIST:
235:             queryBuilder.setTables(SQLITE_TABLE_TEAMS);
236:             cursor = queryBuilder.query (db, projection, selection,
237:                 selectionArgs, groupBy, having, sortOrder);
238:             break;
239:             
240:         case TEAMS_ID:
241:             // id of team is at second position
242:             String teamId = uri.getPathSegments().get(1);
243:             queryBuilder.setTables(SQLITE_TABLE_TEAMS);
244:             queryBuilder.appendWhere(LeagueDataContract.Teams._ID + " = " + teamId);
245:             cursor = queryBuilder.query (db, projection, selection,
246:                 selectionArgs, groupBy, having, sortOrder);
247:             break;
248:             
249:         case GAMES_LIST:
250:             queryBuilder.setTables(SQLITE_TABLE_GAMES);
251:             cursor = queryBuilder.query (db, projection, selection,
252:                 selectionArgs, groupBy, having, sortOrder);
253:             break;
254:             
255:         case GAMES_ID:
256:             // id of game is at second position
257:             String gameId = uri.getPathSegments().get(1);
258:             queryBuilder.setTables(SQLITE_TABLE_GAMES);
259:             queryBuilder.appendWhere(LeagueDataContract.Games._ID + " = " + gameId);
260:             cursor = queryBuilder.query (db, projection, selection,
261:                 selectionArgs, groupBy, having, sortOrder);
262:             break;            
263:             
264:         case LEAGUE_TABLE:
265:             // applying a raw SQLite data access
266:             if (selectionArgs == null) { // no match day specified
267:                 cursor = db.rawQuery(DATATABLE_LEAGUETABLE, null);
268:             }
269:             else { // match day specified
270:                 rawSql =
271:                     String.format(DATATABLE_LEAGUETABLE_MATCHDAY, selectionArgs[0]);
272:                 cursor = db.rawQuery(rawSql, null);
273:             }
274:             break;
275:             
276:         case TEAMS_LIST_COUNT:
277:             rawSql = "SELECT COUNT(*) FROM " + SQLITE_TABLE_TEAMS;
278:             cursor = db.rawQuery (rawSql, null);            
279:             break;
280:         
281:         case GAMES_LIST_COUNT:
282:             rawSql = "SELECT COUNT(*) FROM " + SQLITE_TABLE_GAMES;
283:             cursor = db.rawQuery (rawSql, null);
284:             break;
285:             
286:         default:
287:             System.out.println("LeagueDataProvider::query: Internal Error !");
288:         }
289: 
290:         // register to watch a content URI for changes
291:         Context context = this.getContext();
292:         ContentResolver cr = context.getContentResolver();
293:         cursor.setNotificationUri(cr, uri);
294:         return cursor;
295:     }
296: 
297:     @Override
298:     public Uri insert(Uri uri, ContentValues values) {
299:         
300:         // adding a record
301:         int match = LeagueDataProvider.uriMatcher.match(uri);
302:         if (match != TEAMS_LIST && match != GAMES_LIST) {  
303:             throw new IllegalArgumentException (
304:                 "Unsupported URI for insertion: " + uri);  
305:         }  
306: 
307:         SQLiteDatabase db = this.dbHelper.getWritableDatabase();
308:         if (db == null) {
309:             System.out.println ("LeagueDataProvider::getWritableDatabase failed !!!");
310:             return null;
311:         }
312:        
313:         long row;
314:         Context context = this.getContext(); 
315:         ContentResolver cr = context.getContentResolver();
316: 
317:         switch (match) {
318:         
319:         case TEAMS_LIST:
320:             // add a new teams record        
321:             row = db.insert(SQLITE_TABLE_TEAMS, null, values);
322:             
323:             // notify all listeners of changes
324:             if (row > 0) {
325:                 Uri uriTeams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
326:                 Uri uriWithId = ContentUris.withAppendedId(uriTeams, row);
327:                 cr.notifyChange(uriWithId, null);
328:                 return uriWithId;
329:             }
330:             break;
331:             
332:         case GAMES_LIST:
333:             // add a new games record
334:             row = db.insert(SQLITE_TABLE_GAMES, null, values);
335: 
336:             // notify all listeners of changes
337:             if (row > 0) {
338:                 Uri uriGames = Uri.parse(LeagueDataContract.CONTENT_GAMES);
339:                 Uri uriWithId = ContentUris.withAppendedId(uriGames, row);
340:                 cr.notifyChange(uriWithId, null);
341:                 return uriWithId;
342:             }
343:             break;
344:             
345:         default:
346:             System.out.println("LeagueDataProvider::insert: Internal Error !");            
347:             throw new IllegalArgumentException("Unsupported URI: " + uri); 
348:         }
349:         
350:         throw new SQLException("Failed to add a record into " + uri);
351:     }
352: 
353:     @Override
354:     public int delete(Uri uri, String selection, String[] selectionArgs) {
355:         
356:         // deleting records
357:         SQLiteDatabase db = this.dbHelper.getWritableDatabase();
358:         if (db == null) {
359:             System.out.println ("LeagueDataProvider::getWritableDatabase failed !!!");
360:             return 0;
361:         }        
362:         
363:         // distinguish between dir based and id based uri's
364:         int match = uriMatcher.match(uri);
365:         int count = -1;
366:         
367:         switch(match) {
368:         
369:             case TEAMS_LIST:
370:                 count = db.delete(SQLITE_TABLE_TEAMS, selection, selectionArgs);
371:                 break;
372:                 
373:             case TEAMS_ID:
374:                 String id = uri.getLastPathSegment();
375:                 String where = LeagueDataContract.Teams._ID + " = " + id;
376:                 if (! TextUtils.isEmpty(selection)) {
377:                     where = where + " AND " + selection;
378:                 }
379:                 count = db.delete(SQLITE_TABLE_TEAMS, where, selectionArgs);
380:                 break;
381:                 
382:             case GAMES_LIST:
383:                 count = db.delete(SQLITE_TABLE_GAMES, selection, selectionArgs);
384:                 break;
385:                 
386:             case GAMES_ID:
387:                 id = uri.getLastPathSegment();
388:                 where = LeagueDataContract.Games._ID + " = " + id;
389:                 if (! TextUtils.isEmpty(selection)) {
390:                     where = where + " AND " + selection;
391:                 }
392:                 count = db.delete(SQLITE_TABLE_GAMES, where, selectionArgs);
393:                 break;
394:             
395:             default:
396:                 System.out.println("LeagueDataProvider::delete: Internal Error !");            
397:                 throw new IllegalArgumentException("Unsupported URI: " + uri); 
398:         }
399:         
400:         // notify all listeners of changes
401:         if (count > 0) {
402:             
403:             Context context = this.getContext();
404:             ContentResolver cr = context.getContentResolver();
405:             cr.notifyChange(uri, null);
406:         }  
407:     
408:         return count;
409:     }
410: 
411:     @Override
412:     public int update(Uri uri, ContentValues values,
413:         String selection, String[] selectionArgs) {        
414:         // modifying data - not implemented
415:         System.out.println ("LeagueDataProvider::update [not implemented]");
416:         return 0;
417:     }
418:     
419:     @Override
420:     public String getType(Uri uri) {
421:         
422:         switch (LeagueDataProvider.uriMatcher.match(uri)) {
423:         case TEAMS_LIST:
424:             return
425:                 ContentResolver.CURSOR_DIR_BASE_TYPE +
426:                 "/vnd.com.example.LeagueDataProvider.teams";
427:         case TEAMS_ID:
428:             return
429:                 ContentResolver.CURSOR_ITEM_BASE_TYPE +
430:                 "/vnd.com.example.LeagueDataProvider.teams";
431:         case GAMES_LIST:
432:             return
433:                 ContentResolver.CURSOR_DIR_BASE_TYPE +
434:                 "/vnd.com.example.LeagueDataProvider.games";
435:         case GAMES_ID:
436:             return
437:                 ContentResolver.CURSOR_ITEM_BASE_TYPE +
438:                 "/vnd.com.example.LeagueDataProvider.games";
439:         case TEAMS_LIST_COUNT:
440:             return
441:                 ContentResolver.CURSOR_ITEM_BASE_TYPE +
442:                 "/vnd.com.example.LeagueDataProvider.teams";
443:         case GAMES_LIST_COUNT:
444:             return
445:                 ContentResolver.CURSOR_ITEM_BASE_TYPE +
446:                 "/vnd.com.example.LeagueDataProvider.games";
447:         case LEAGUE_TABLE:
448:             return
449:                 ContentResolver.CURSOR_DIR_BASE_TYPE +
450:                 "/vnd.com.example.LeagueDataProvider.table";
451:         default:
452:             System.out.println("LeagueDataProvider::getType: Internal Error !");       
453:             throw new IllegalArgumentException("Unsupported URI: " + uri);        
454:         }
455:     }
456: }

Beispiel 2. Klasse LeagueDataProvider: Implementierung der Content Provider Kernfunktionalität.


Zugegeben, die Realisierung eines kompletten Content Providers ist nicht gerade kurz geraten. Aus diesem Grund heben wir im Folgenden die wesentlichen Aspekte der Implementierung hervor:

Verwaltung der Daten

Nach den Festlegungen für die Namensgebung eines Android Content Providers wenden wir uns den Daten selbst zu. Für diese Fallstudie bieten sich zwei Tabellen innerhalb des SQLite-Datenbanksystems an. Für das Datenbanksystem selbst gibt es eine Hilfsklasse SQLiteOpenHelper, die zu spezialisieren ist. In den Zeilen 147 bis 165 von Listing 2 finden Sie zu diesem Zweck eine (geschachtelte) Klasse DatabaseHelper vor, die diese Aufgabe übernimmt. Auf diese Weise reduziert sich der Entwicklungsaufwand auf die Implementierung zweier Methoden

public void onCreate (SQLiteDatabase db);

und

public void onUpgrade (SQLiteDatabase db, int oldVersion, int newVersion);

Die erste Methode onCreate ist nicht zu verwechseln mit einer gleichnamigen Methode, die im Kontext des Content Providers zu implementieren ist:

public void onCreate ();

Die onCreate-Methode des Content Providers wird (im primären Thread der App) aufgerufen, wenn in einem laufenden Android Betriebssystem der Prozess des Content Providers benötigt wird. Ich habe diese Formulierung so gewählt, um zum Ausdruck zu bringen, dass diese Methode eben auch dann aufgerufen werden kann, wenn mit ihrer Hilfe das Tabellenwerk des Providers schon zu einem früheren Zeitpunkt erstmalig eingerichtet wurde und man zu einem späteren Zeitpunkt nur noch auf die vorhandenen Tabellen zugreifen möchte. Man beachte deshalb: Die SQLite-Anweisungen zum Erzeugen entsprechender SQL-Tabellen sehen so aus:

CREATE TABLE IF NOT EXISTS TEAMS ( 
    ID INTEGER PRIMARY KEY NOT NULL,
    NAME TEXT NOT NULL,
    CITY TEXT NOT NULL
);

Der Zusatz IF NOT EXISTS stellt sicher, dass diese Anweisung prinzipiell öfters aufgerufen werden kann, da sie die Tabelle nur für den Fall erzeugt, wenn diese noch nicht vorhanden ist. Andernfalls geht von dieser Anweisung keine Wirkung aus.

Zugriff auf einzelne Werte

Nicht alle Anfragen an einen Content Provider besitzen als Ergebnis ein Cursor-Objekt, das eine oder mehrere Reihen einer durch den Provider verwalteten Datenbanktabelle (in Ausschnitten) beschreibt. Denken Sie zum Beispiel an die einfache Fragestellung, wie viele Mannschaften oder Spielresultate im Provider abgelegt sind. Mit einem kleinen Kunstgriff lassen sich auch solche Abfragen durch das Content Provider Regelwerk darstellen. Im Provider selbst hilft hier in der Regel eine raw database-Abfrage weiter. Eine derartige Abfrage wird nicht mit Hilfe eines SQLiteQueryBuilder-Hilfsobjekts formuliert, sondern direkt in Standard-SQL-Syntax an das Datenbanksystem gerichtet. Beispielsweise liefert das Ergebis eines SELECT COUNT(*) FROM TABLE-Abfrageausdrucks einen elementaren Wert zurück. Dieser Wert kann im Sinne einer id-basierten URI-Abfrage im Client abgefragt werden. In den Zeilen 276 bis 279 wie auch 281 bis 284 werden zur Bestimmung der Anzahl der vorhandenen Mannschaften bzw. Spielresultate entsprechende SQL-Anweisungen direkt an das SQLite-Datenbanksystem gestellt. Auch hierbei wird das Ergebnis in einem Cursor-Objekt zurückgeliefert, allerdings in einem etwas leichtgewichtigen Objekt, das nur aus einer Reihe mit einer Spalte besteht. Beim Zugriff auf dieses Objekt spielt der Name der Spalte keine Rolle, mit dem Index 0 greift man direkt auf den Wert der (einzigen) Spalte zu.

Definitionen für die UriMatcher-Klasse

Der Kontrakt zwischen einem Content Provider und seinen Clients erfolgt auf der Basis von Zeichenketten. Genauer gesprochen: Mit Hilfe so genannter Autoritäten (Hauptkennung eines Content Providers), den Pfaden (Teilkennung innerhalb eines Content Providers) und optional auch noch einer Id (int-Wert). Da diese Zeichenketten alle für sich (weltweit) eindeutig sein müssen, können wir auch von so genannten URI's (Uniform Resource Identifier) sprechen. Der int-Wert spielt dann eine Rolle, wenn man innerhalb einer Datenmenge einen Datensatz explizit ansprechen möchte, zum Beispiel die 3. Mannschaft oder das 18. Spiel. In diesem Zusammenhang spricht man auch von einer so genannten id-basierten URI. Möchte man eine Datenmenge als Ganzes ansprechen, lässt man den ganzzahligen Wert am Ende einfach weg und wir sprechen von einer dir-basierten URI.

Spricht ein Client einen Content Provider an, übermittelt er eine URI, um die Datenmenge (oder ein einzelnes Element daraus) zu identifizieren. Eine Teilaufgabe des Content Providers besteht nun darin, all diese Zeichenketten zu parsen, um an Hand ihres Aufbaus die spezifizierte Datenmenge zu erkennen. Zu diesem Zweck gibt es in der Android-Klassenbibliothek eine Hilfsklasse UriMatcher. Sie vereinfacht das Analysieren von URI's ganz erheblich. Im Prinzip verwaltet ein UriMatcher-Objekt intern ein Assoziativ-Array (Map, Dictionary), das als nichtnumerischen Schlüssel eine URI verwendet und dieser einen korrespondieren ganzzahligen Wert zuordnet. Übermittelt ein Client nun eine URI an den Content Provider, kann dieser mit Hilfe des UriMatcher-Objekts den korrespondieren int-Wert ermitteln und auf diese Weise einfacher die Anfrage erkennen.

In unserer Fallstudie wollen wir den Zugriff auf die Mannschaften, die Spielergebnisse und auf die Ligatabelle ermöglichen. Deshalb benötigen wir drei Unter-Autoritäten:

content://com.example.LeagueDataProvider/teams
content://com.example.LeagueDataProvider/games
content://com.example.LeagueDataProvider/table

Als Haupt-Autorität können Sie dabei die Zeichenkette com.example.LeagueDataProvider erkennen.

Nebenbemerkung: Nicht jede mit einer Autorität assoziierte Information muss im Content Provider einer SQLite-Datenbanktabelle entsprechen. Im Falle der Ligatabelle gibt es in meiner Realisierung keine zugeordnete Datenbanktabelle. Die Information wird on demand beim Zugriff an Hand der Informationen in den anderen beiden Datenbanktabellen generiert und übertragen.

Alle Anfragen zu Mannschaften und Spielergebnissen ergeben sowohl id-basiert wie auch dir-basiert Sinn. Bei der Ligatabelle ist die Lage etwas anders: Man kann zwar zwischen der Ligatabelle zu einem bestimmten Spieltag und der aktuellen Ligatabelle unterscheiden. In beiden Fällen haben wir es aber mit einem dir-basierten Zugriff zu tun, da pro Anfrage eine bestimmte Menge von Tabelleneinträgen zu übertragen ist. Die (optionale) Differenzierung in der Anfrage bzgl. eines bestimmten Spieltages kann durch den Selektions-Parameter in der query-Anweisung übertragen werden, der letzten Endes einer SQL-WHERE-Klausel entspricht.

Auf eine kleine Besonderheit sollten wir noch eingehen: Unter welche Kategorie fällt eine Abfrage nach dem Motto „Wie viele Mannschaften gibt es?“ oder „Wie viele Spielergebnisse liegen vor?“? Da die Antwortmenge aus einem einzigen Wert besteht, scheidet eine dir-Realisierung aus. Mit etwas Phantasie lassen sich solche Fragen als id-basierte Abfragen einstufen.

Wenn wir nun auf die UriMatcher-Definition eingehen, kommen insgesamt sieben unterschiedliche Zeichenkettenformate für die diversen Anfragen in Frage:

content://com.example.LeagueDataProvider/teams
content://com.example.LeagueDataProvider/teams/#
content://com.example.LeagueDataProvider/games
content://com.example.LeagueDataProvider/games/#
content://com.example.LeagueDataProvider/teams/count
content://com.example.LeagueDataProvider/games/count
content://com.example.LeagueDataProvider/table

Um das Zerlegen und Parsen diverser möglicher, realer Anfragen, wie zum Beispiel

content://com.example.LeagueDataProvider/teams/10

oder

content://com.example.LeagueDataProvider/teams/count

software-unterstützt vornehmen zu lassen, bildet in der vorliegenden Realisierung die Klasse UriMatcher diese Zeichenketten auf die folgenden ganzen Zahlen ab:

private static final int TEAMS_LIST = 1;  
private static final int TEAMS_ID = 2;  
private static final int GAMES_LIST = 3;  
private static final int GAMES_ID = 4;  
private static final int TEAMS_LIST_COUNT = 5;
private static final int GAMES_LIST_COUNT = 6;
private static final int LEAGUE_TABLE = 7; 

Konfiguration des Content Providers

Im Manifest-File ist der Content Provider zu registrieren. Im konkret vorliegenden Beispiel sieht dies so aus:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <manifest
04:     xmlns:android="http://schemas.android.com/apk/res/android"
05:     package="com.example.leaguedataprovider"
06:     android:versionCode="1"
07:     android:versionName="0.61" >
08:     
09:     <uses-sdk
10:         android:minSdkVersion="8"
11:         android:targetSdkVersion="19" />
12:     
13:     <permission
14:         android:name="com.example.leaguedataprovider.READ_ONLY"
15:         android:protectionLevel="normal"/>
16:     
17:     <permission
18:         android:name="com.example.leaguedataprovider.WRITE"
19:         android:protectionLevel="normal"/>
20: 
21:     <application
22:         android:allowBackup="true"
23:         android:icon="@drawable/ic_launcher"
24:         android:label="@string/app_name"
25:         android:theme="@style/AppTheme" >
26:         
27:         <activity
28:             android:name="com.example.leaguedataprovider.MainActivity"
29:             android:label="@string/app_name" >
30:             <intent-filter>
31:                 <action android:name="android.intent.action.MAIN" />
32:                 <category android:name="android.intent.category.LAUNCHER" />
33:             </intent-filter>
34:         </activity>
35:         
36:         <provider
37:             android:name="LeagueDataProvider"   
38:             android:exported="true"
39:             android:readPermission="com.example.leaguedataprovider.READ_ONLY"
40:             android:writePermission="com.example.leaguedataprovider.WRITE"
41:             android:authorities="com.example.LeagueDataProvider"> 
42:         </provider>
43:         
44:     </application>
45: </manifest>

Beispiel 3. Manifest-Datei eines Content Providers.


Die wesentlichen Zeilen in der Manifest-Datei finden Sie in Listing 3 von 36 bis 42 vor. Hier wird beschrieben, dass die Android-App eine Content Provider Funktionalität zur Verfügung stellt. Unter android:name ist der Klassenname des Providers abzulegen. Das Konzept der Autoritäten haben wir bereits angesprochen. In Zeile 41 finden Sie die von mir (leider etwas einfallslos geratene) Zeichenkette com.example.LeagueDataProvider vor. Die Clienten eines Content Providers sind typischerweise andere Apps. Aus diesem Grund ist die Provider-Funktionalität mit dem Attribut android:exported="true" zu markieren, um auf diese außerhalb des Host-Prozesses zugreifen zu dürfen. Zwei weitere Attribute (android:readPermission und android:writePermission) behandeln das Thema der „Zugriffsrechte“. Darauf gehen wir im zweiten Teil dieser Fallstudie näher ein.

Die größten Hürden in der Implementierung eines Content Providers haben wir bereits genommen. Für die in der Aufgabenstellung gezeigte App (Abbildung 1) benötigen wir noch ein XML-Layoutfile (Listing 4) und eine korrespondierende Java-Datei (Listing 5) für den Logik-Anteil (sowie eine Datei zur Ablage der Zeichenketten-Ressourcen):

01: <RelativeLayout
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     android:layout_width="fill_parent"
04:     android:layout_height="fill_parent"
05:     android:scrollbars="vertical"
06:     android:layout_weight="1">
07:     
08:     <TextView
09:         android:id="@+id/textViewHeader"
10:         android:layout_width="match_parent"
11:         android:layout_height="wrap_content"
12:         android:layout_margin="5dp"
13:         android:textSize="22sp"
14:         android:text="@string/textViewHeader" />
15:     
16:     <View
17:         android:id="@+id/viewSeperator"
18:         android:layout_width="fill_parent"
19:         android:layout_height="3dip"
20:         android:background="#FF000000"
21:         android:layout_marginBottom="10dip"
22:         android:layout_below="@+id/textViewHeader" />
23: 
24:     <TableLayout
25:         android:id="@+id/tableLayoutAnchor"
26:         android:layout_width="fill_parent"
27:         android:layout_height="wrap_content"
28:         android:layout_below="@+id/viewSeperator" >
29:     
30:         <TableRow>  
31:             <TextView
32:                 android:layout_width="match_parent"
33:                 android:layout_height="wrap_content"
34:                 android:layout_margin="5dp"
35:                 android:textSize="20sp"
36:                 android:text="@string/textViewVersion" />
37:             
38:             <TextView
39:                 android:id="@+id/textViewVersion"
40:                 android:layout_width="match_parent"
41:                 android:layout_height="wrap_content"
42:                 android:layout_marginLeft="30dp"
43:                 android:textSize="15sp" />
44:         </TableRow>
45:         
46:         <TableRow>  
47:             <TextView
48:                 android:layout_width="match_parent"
49:                 android:layout_height="wrap_content"
50:                 android:layout_margin="5dp"
51:                 android:textSize="20sp"
52:                 android:text="@string/textViewTeamsCount" />
53:      
54:             <TextView
55:                 android:id="@+id/textViewTeamsCount"
56:                 android:layout_width="match_parent"
57:                 android:layout_height="wrap_content" 
58:                 android:layout_marginLeft="30dp"
59:                 android:textSize="15sp" />
60:         </TableRow>
61:         
62:         <TableRow>  
63:             <TextView
64:                 android:layout_width="match_parent"
65:                 android:layout_height="wrap_content"
66:                 android:layout_margin="5dp"
67:                 android:textSize="20sp"
68:                 android:text="@string/textViewGamesCount" />
69:                     
70:             <TextView
71:                 android:id="@+id/textViewGamesCount"
72:                 android:layout_width="match_parent"
73:                 android:layout_height="wrap_content" 
74:                 android:layout_marginLeft="30dp"
75:                 android:textSize="15sp" /> 
76:         </TableRow>
77:     </TableLayout>
78:     
79: </RelativeLayout>

Beispiel 4. Datei activity_main.xml: UI der Hauptaktivität (Content Provider App).


Die Zeichenketten-Ressourcen sind dabei so definiert:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">LeagueData: Content Provider</string>
    <string name="action_settings">Settings</string>
    
    <string name="textViewHeader">Status</string>
    <string name="textViewTeamsCount">Table Teams:</string>
    <string name="textViewGamesCount">Table Games:</string>
    <string name="textViewVersion">Version:</string>

</resources>

Fehlt noch die Java-Datei für den Logik-Anteil (Listing 5):

01: package com.example.leaguedataprovider;
02: 
03: import android.app.Activity;
04: import android.content.ContentResolver;
05: import android.database.Cursor;
06: import android.net.Uri;
07: import android.os.Bundle;
08: import android.widget.TextView;
09: 
10: public class MainActivity extends Activity {
11:     
12:     private int tableTeamsRows;
13:     private int tableGamesRows;
14:     
15:     public MainActivity ()
16:     {
17:         super();
18:         
19:         // just for testing ...
20:         System.out.println ("c'tor MainActivity() LeagueData Provider");
21: 
22:         this.tableTeamsRows = 0;
23:         this.tableGamesRows = 0;
24:     }
25: 
26:     @Override
27:     protected void onCreate(Bundle savedInstanceState) {
28:         super.onCreate(savedInstanceState);
29:         setContentView(R.layout.activity_main);
30:         
31:         // retrieve current status of league provider
32:         this.getStatusInformations();
33:         this.updateMainView();
34:     }
35: 
36:     private void getStatusInformations() {
37:         
38:         ContentResolver cr = this.getContentResolver();
39:         
40:         Uri uriTeams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
41:         Uri uriTeamsCount = Uri.withAppendedPath(uriTeams, "count");
42:         
43:         Cursor cursor = cr.query(uriTeamsCount, null, null, null, null);
44:         
45:         if (cursor == null) {
46:             System.out.println ("Internal ERROR: Cursor IS NULL");
47:         }
48:         else if (! cursor.moveToFirst()) {
49:             System.out.println("  No content currently !");
50:         }
51:         else {
52:             this.tableTeamsRows = cursor.getInt(0);
53:             cursor.close();
54:             System.out.println("  TEAMS COUNT = " + this.tableTeamsRows);
55:         }
56:         
57:         Uri uriGames = Uri.parse(LeagueDataContract.CONTENT_GAMES);
58:         Uri uriGamesCount = Uri.withAppendedPath(uriGames, "count");
59:         
60:         cursor = cr.query(uriGamesCount, null, null, null, null);
61:         
62:         if (cursor == null) {
63:             System.out.println ("Internal ERROR: Cursor IS NULL");
64:         }
65:         else if (! cursor.moveToFirst()) {
66:             System.out.println("  No content currently !");
67:         }
68:         else {
69:             this.tableGamesRows = cursor.getInt(0);
70:             cursor.close();
71:             System.out.println("  GAMES COUNT = " + this.tableGamesRows);
72:         }
73:     }
74:     
75:     private void updateMainView() {
76:         
77:         TextView textViewVersion =
78:             (TextView) this.findViewById(R.id.textViewVersion);
79:         textViewVersion.setText (LeagueDataContract.LEAGUE_PROVIDER_VERSION);
80: 
81:         TextView textViewTeamsCount =
82:             (TextView) this.findViewById(R.id.textViewTeamsCount);
83:         textViewTeamsCount.setText (
84:             String.valueOf(this.tableTeamsRows) + " Records");
85: 
86:         TextView textViewGamesCount =
87:             (TextView) this.findViewById(R.id.textViewGamesCount);
88:         textViewGamesCount.setText (
89:             String.valueOf(this.tableGamesRows) + " Records");
90:     }
91: }

Beispiel 5. Datei MainActivity.java: Logik-Anteil der Hauptaktivität (Content Provider App).