Content provider

Definició[modifica]

Un proveïdor de contingut administra l’accés a un repositori central de dades. Un proveïdor s’implementa com a una o més classes dins d’una aplicació Android, juntament amb elements en l’arxiu de manifest.

Una d’aquestes classes ha d’implementar una subclasse ContentProvider, què és la interfície entre el nostre proveïdor de contingut i altres aplicacions.

Els proveïdors de contingut permeten que les dades estiguin disponibles per a altres aplicacions, però a més, poden haver-hi activitats a les nostres aplicacions què li permetin a l’usuari consultar i modificar les dades administrades per el proveïdor.

Disseny de l'emmagatzematge de dades[modifica]

Un proveïdor de contingut és la interfície per a guardar dades en un format estructurat. Però abans de crear la interfície hem de decidir com guardarem les dades. Es poden guardar les dades en qualsevol format i després dissenyar la interfície per tal de poder llegir i escriure les dades tal i com sigui necessari.

A continuació mostro algunes de les tecnologies d’emmagatzematge de dades disponibles en Android:

  • El sistema Android inclou una SQLite Database API que els mateixos proveïdors utilitzen per guardar dades orientades a taules. Disposem de la classe SQLiteOpenHelper i de la classe SQLiteDatabase què ens ajudaran a implementar el nostre Content Provider. La primera classe ens ajudarà a crear bases de dades i la segona la utilitzarem per a accedir a aquestes bases de dades.
  • Per guardar dades d’arxiu, Android té una gran varietat de API orientades a arxius. Podem veure més informació sobre aquest tema a Emmagatzematge de dades.
  • També podem treballar amb dades basades a la xarxa, amb classes com java.net i android.net. A Exemple d’adaptador de sincronització es mostra com funciona.

Disseny de URI de contingut[modifica]

Un URI (Uniform Resource Identifier) és una cadena de caràcters curta que identifica inequívocament un recurs. La diferència principal que aquests tenen respecte una URL (Uniform Resource Locator) és que aquests últims poden canviar amb el pas del temps.

Un URI de contingut és un URI que identifica dades d’un proveïdor. Els URI de contingut inclouen el nom simbòlic de tot el seu proveïdor (autoritat) i un nom que apunta a una taula o arxiu (ruta d’accés). Cada mètode d’accés a dades del ContentProvider té un URI de contingut en forma d’argument, amb el qual podem accedir a una taula, la fila, o l’arxiu al que s’accedirà.

A la següent taula podem veure com es relaciona una consulta query() amb una consulta SQL:

Argument de query() Paraula clau/paràmetre SELECT Comentaris
Uri FROM table_name Uri. S'assigna a la taula del proveïdor anomenada table_name
projection col, col, col,... projection. És una matriu de columnes que deu incloure's per a cada fila recuperada
selection WHERE col = value selection. Especifica els criteris per a seleccionar files
selectionArgs Els elements de selecció reemplaçen als marcadors de posició ? a la clàusula de selecció
sortOrder ORDER BY col, col, col, ... sortOrder. Especifica l'ordre en el que apareixen les files al Cursor mostrat

Disseny de l'autoritat[modifica]

Un proveïdor generalment té una única autoritat, que serveix com al seu nom intern en Android. Per a evitar conflictes amb altres proveïdors, alhora de constuir una autoritat, ho farem com a propietat de dominis d’internet (en sentit invers).

Per exemple si tenim un projecte Android amb el paquet com.adrian.lamevaaplicacio, llavors al nostre proveïdor li hauríem de proporcionar l’autoritat de la següent manera: com.adrian.lamevaaplicacio.provider.

Disseny de l'estructura d'una ruta d'accés[modifica]

Els desenvolupadors generalment creen URI de contingut a partir de l’autoritat annexant rutes d’accés que apunten a taules individuals. Per referenciar aquestes taules ho podem fer de la següent manera:

Suposant que tenim dues taules (taula1 i taula2) podíem utilitzar l’autoritat anterior i annexant els noms de les taules per tal d’obtenir les URI de contingut, les quals quedarien aixi:

  • com.adrian.lamevaaplicacio.provider/taula1
  • com.adrian.lamevaaplicacio.provider/taula2

Manipulació de ID de URI de contingut[modifica]

Per  conveni, els proveïdors ofereixen accés a una sola fila de la taula a l'acceptar un URI de contingut amb un valor de ID per a la fila corresponent, indicat al final del URI. També comparen el valor del ID proporcionat amb la columna _ID de la taula i realitzen l’accés sol·licitat en funció de la fila corresponent.

Aquest conveni facilita un patró de disseny comú per a totes les aplicacions que volen accedir a un proveïdor. L’aplicació realitza una consulta al proveïdor i mostra el Cursor resultant en una ListView.

Després l’usuari selecciona una de les files possibles i l’aplicació obté la fila corresponent mitjançant el Cursor, què obté el valor _ID per a aquesta fila, ho annexa al URI de contingut i envia la sol·licitud d’accés al proveïdor. Amb tot això el proveïdor pot realitzar l’operació corresponent sobre aquesta fila seleccionada per l’usuari.

Patrons de URI de contingut[modifica]

Per a ajudar a seleccionar l’acció desitjada per a una URI de contingut entrant, la Provider API disposa de la classe UriMatcher, que assigna “patrons” de URI de contingut a valors enters. Aquests valors enters normalment s’utilitzen en una comparació switch que selecciona l’acció desitjada per els URI de contingut que coincideixen amb un patró específic.

Alguns exemples de URI de contingut que apunten a diferents taules son:

  • content://com.adrian.lamevaaplicacio.provider/taula1
  • content://com.adrian.lamevaaplicacio.provider/taula2/subtaula1

El proveïdor també pot reconèixer aquells URI que continguin un ID annexat a ells, com per exemple:

  • content://com.adrian.lamevaaplicacio.provider/taula1/1

Això ens indicaria que volem un accés a la fila identificada amb un “1” de la taula1.

Podem trobar el mètode addURI() el qual assigna una autoritat i una ruta d’accés a un valor enter. El mètode match() retorna el valor enter assignat a un URI en concret.

A continuació podeu veure un fragment de codi on assignarem a un uriMatcher una ruta d'accés a un valor enter:

static final int uriCode = 1;
static final UriMatcher uriMatcher;

static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(PROVIDER_NAME, "cpcontacts", uriCode);
}

Implementació de la classe ContentProvider[modifica]

La instància ContentProvider administra l’accés a un conjunt de dades estructurades mitjançant la manipulació de sol·licituds d’altres aplicacions. Totes les formes d’accés eventualment criden a ContentResolver, que després crida a un mètode en concret de ContentProvider per tal de poder obtenir accés.

Mètodes necessari[modifica]

La classe ContentProvider defineix sis mètodes què hem d'implementar com a part de la nostra pròpia subclasse. Aquests mètodes, excepte el mètode onCreate(), els pot cridar una aplicació client que intenta accedir al nostre proveïdor de contingut, i són els següents:

onCreate()[modifica]

Inicialitza el proveïdor. Aquest mètode es cridat pel sistema Android immediatament després de crear el proveïdor. Hem de tenir en compte què el nostre proveïdor no es crea fins que un objecte ContentResolver intenta accedir a ell.

query()[modifica]

Recupera les dades del proveïdor. Utilitza arguments per tal de poder seleccionar la taula que volem consultar, les files i les columnes que volem retornar i l’ordre dels resultats. Retorna les dades en forma d’objecte Cursor.

insert()[modifica]

Inserta una nova fila en el nostre proveïdor. Utilitza els arguments per tal de seleccionar la taula destí i obtenir els valors de columna que s’utilitzaran. Retorna un URI de contingut per a la nova fila insertada.

update()[modifica]

Actualitza les files existents en el nostre proveïdor. utilitza els arguments per a seleccionar la taula destí i obtenir els valors de la columna que s’utilitzarà. Retorna el número de files actualitzades.

delete()[modifica]

Elimina files del nostre proveïdor. Utilitza els arguments per seleccionar la taula i les files que s’eliminaran. Retorna el número de files eliminades.

getType()[modifica]

Retorna el tipus de MIME al URI de contingut.

Implementació dels mètodes[modifica]

A continuació os mostraré uns fragments de codi què he fer per a realitzar la meva pràctica on podeu veure com està implementat cada mètode:

Implementació del mètode onCreate()[modifica]

El sistema Android crida a onCreate() quan s’inicia el proveïdor. En aquest mètode només s’han de realitzar tasques d’inicialització ràpida i hem d’intentar crear la base de dades i la càrrega de dades fins a què el proveïdor rebi una sol·licitud de dades.

Per altra banda, si realitzem tasques més extenses en el mètode onCreate() l’inicialització del nostre proveïdor serà més lenta.

@Override
public boolean onCreate() {
    DatabaseHelper helper = new DatabaseHelper(getContext());
    database = helper.getWritableDatabase();

    if(database != null) {
        return true;
    }

    return false;
}

Implementació del mètode query()[modifica]

El mètode query() ha de retornar un objecte de tipus Cursor, en cas que tot vagi bé, però si alguna cosa falla, llavors s’ha de produir una excepció.

Si estem utilitzant una base de dades SQLite com a punt d’emmagatzematge, llavors podem simplement retornar el Cursor retornat per un dels mètodes query() què ens proporciona la classe SQLiteDatabase. En cas que la consulta no coincideixi amb cap fila, s’ha de retornar una instància Cursor on el mètode getCount() retorni 0. Per altra banda si es produeix un error intern en el procés de consulta haurem de retornar null.

Els tipus d’excepcions que podem utilitzar per manipular errors de consulta són:

  • IllegalArgumentException (Normalment s’utilitza aquesta si el nostre proveïdor rep un URI de contingut no vàlid).
  • NullPointerException
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
    SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
    queryBuilder.setTables(TABLE_NAME);

    switch (uriMatcher.match(uri)) {
        case uriCode:
            queryBuilder.setProjectionMap(values);
            break;
        default:
            throw new IllegalArgumentException("URI desconocida : " + uri);
    }

    Cursor cursor = queryBuilder.query(database, strings, s,strings1, null, null, s1);
    cursor.setNotificationUri(getContext().getContentResolver(), uri);

    return cursor;
}

Implementació del mètode insert()[modifica]

El mètode insert() agrega una nova fila a la taula corresponent utilitzant els valors de l’argument ContentValues. Si el nom d’una columna no està a l’argument ContentValues, pots proporcionar un valor predeterminat per a ell ja sigui en el codi del proveïdor o en l’esquema de la teva base de dades.

Aquest mètode ha de retornar l’URI de contingut per a la nova fila. Per fer això ho fem mitjançant el mètode withAppendedId(), què annexa el nou valor _ID de la fila a l’URI de contingut de la taula.

@Override
public Uri insert(Uri uri, ContentValues contentValues) {
    long idFila = database.insert(TABLE_NAME, null, contentValues);

    if(idFila > 0) {
        Uri _uri = ContentUris.withAppendedId(CONTENT_URL, idFila);
        getContext().getContentResolver().notifyChange(_uri, null);
        
        return _uri;
    }
    else {
        Toast.makeText(getContext(), "Error al insertar la fila", Toast.LENGTH_SHORT).show();
        return null;
    }
}

Implementació del mètode update()[modifica]

El mètode update() pren com a arguments ContentValues els mateixos que el mètode insert() i els mateixos arguments selection i  selectionArgs utilitzats pel mètode delete() i query(). D’aquesta manera és molt possible que puguem reutilitzar codi entre aquests mètodes.

@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
    int filesActualitzades = 0;

    switch (uriMatcher.match(uri)) {
        case uriCode:
            filesActualitzades = database.update(TABLE_NAME, contentValues, s, strings);
            break;
        default:
            throw new IllegalArgumentException("URI desconocida : " + uri);
    }

    getContext().getContentResolver().notifyChange(uri, null);

    return filesActualitzades;
}

Implementació del mètode delete()[modifica]

El mètode delete() no té per què eliminar les files físicament del punt d’emmagatzematge de dades. Encara què es pot, també es possible símplement marcar la fila com a esborrada mitjançant un camp de la taula.

@Override
public int delete(Uri uri, String s, String[] strings) {
    int filesEsborrades = 0;

    switch (uriMatcher.match(uri)) {
        case uriCode:
            filesEsborrades = database.delete(TABLE_NAME, s, strings);
            break;
        default:
            throw new IllegalArgumentException("URI desconocida : " + uri);
    }

    getContext().getContentResolver().notifyChange(uri, null);

    return filesEsborrades;
}

Implementació del mètode getType()[modifica]

El mètode getType() retorna un String en format MIME que descriu el tipus de dades retornades per l’argument del URI de contingut.

Per als URI de contingut que apunten a una fila o files de dades d’una taula, getType() ha de retornar un tipus de MIME en un format MIME específic per a proveïdors d’Android:

  • Part de tipus: vnd
  • Part de subtipus:
    • Si el patró del URI és per a una sola fila: android.cursor.item/
    • Si el patró del URI és per a més d’una fila: android.cursor.dir/
  • Part específica pel proveïdor: vnd.<nom>.<tipus>
    • S’ha de proporcionar un <nom> i un <tipus>. El <nom> ha de ser únic i pot ser per exemple, el nom de l’empresa o alguna part del nom del paquet d’Android de la teva aplicació El <tipus> ha de ser únic per al patró de URI corresponent i pot tenir com a valor un string que identifiqui la taula asociada amb el URI.

Per exemple, si l’autoritat de un proveïdor és com.adrian.lamevaaplicacio.provider i té una taula amb el nom taula1, els tipus de MIME podrien ser el següents:

  • MIME per a múltiples files de la taula1
    • vnd.android.cursor.dir/vnd.com.adrian.lamevaaplicacio.provider.taula1
  • MIME per a una sola fila de la taula1
    • vnd.android.cursor.item/vnd.com.adrian.lamevaaplicacio.provider.taula1
@Override
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)) {
        case uriCode:
            return "http://vnd.android .cursor.dir/cpcontacts";
        default:
            throw new IllegalArgumentException("URI no soportada : " + uri);
    }
}

L'element provider[modifica]

Per a què tot funcioni correctament hem de declarar una subclasse de ContentProvider a l'arxiu de manifest utilitzant l'element <provider>. El sistema Android obté la següent informació de l'element:

Autoritat(android:authorities)

Noms simbòlics que identifiquen al proveïdor complet en el sistema.

Nom de la classe del proveïdor(android:name)

La classe que implementa ContentProvider.

Permisos

Atributs que especifiquen els permisos que altres aplicacions han de tenir per poder accedir a les dades del proveïdor:

  • android:grantUriPermission: Indicador de permís temporal.
  • android:permission: Permís individual de lectura/escriptura en tot el proveïdor
  • android:readPermission: Permís de lectura en tot el proveïdor.
  • android:writePermission: Permís d'escriptura en tot el proveïdor.

Atributs de inici i control

Aquests atributs determinen com i quan el sistema Android inicia el proveïdor, les característiques del procés i altres configuracions de temps d'execució:

  • android:enabled: Indicador que li permet al sistema iniciar el proveïdor.
  • android:exported: Indicador que permet que altres aplicacions utilitzin aquest proveïdor.
  • android:initOrder: L'ordre en que s'ha d'iniciar el proveïdor en funció d'altres proveïdors en el mateix procés.
  • android:multiProcess: Indicador que li permet al sistema iniciar el proveïdor en el mateix procés que el client que realitza la crida.
  • android:process: Nom del procés en que s'ha d'executar el proveïdor.
  • android:syncable: Indicador que senyala que les dades del proveïdor es sincronitzen amb les dades de un servidor.

Atributs informatius

  • android:icon: Un recurs de l'element de disseny que conté una icona pel proveïdor. La icona apareix junt a l'etiqueta del proveïdor en la llista d'aplicacions.
  • android:label: una etiqueta informativa que descriu el proveïdor o les seves dades, o ambdós.

En el meu ContentProvider, dins de l'arxiu manifest ho he declarat de la següent manera:

<provider
    android:authorities="com.adrian.contentprovider.ContentProvider"
    android:name=".ContentProvider"
    android:exported="true"
    android:multiprocess="true">
</provider>

La meva implementació del Content Provider[modifica]

Per a poder veure tota la teoria explicada anteriorment d'una manera més interactiva, he creat dues aplicacions per poder veure com funciona el Content Provider.

Les dues classes que he creat son les següents:

  • ContentProvider
  • ContentResolver

La classe ContentResolver és una classe és una classe que ens permet fer les següents accions:

  • Afegir contactes al nostre ContentProvider
  • Buscar contactes al nostre ContentProvider
  • Eliminar contactes del nostre Content Provider
  • Mostrat tots els contactes del nostre Content Provider

A continuació podeu veure les funcions què he fet per tal de poder executar totes les accions anteriors:

Afegir contactes[modifica]

public void afegirContacte(View view) {
    String nomAAfegir = editText.getText().toString();
    ContentValues values = new ContentValues();

    values.put("name", nomAAfegir);

    resolver.insert(CONTENT_URL, values);
    editText.setText("");
    obtenirContactes();
}

Buscar contactes[modifica]

public void buscarContacte(View view) {
    String idATrobar = editText.getText().toString();
    String[] dades = new String[]{"id", "name"};
    Cursor cursor = resolver.query(CONTENT_URL, dades, "id = ?", new String[]{idATrobar}, null);
    String contacte = "";

    if(cursor.moveToFirst()) {
        String id = cursor.getString(cursor.getColumnIndex("id"));
        String nom = cursor.getString(cursor.getColumnIndex("name"));

        contacte += id + " : " + nom + "\n";
    }
    else {
        Toast.makeText(getBaseContext(), "El contacte no s'ha trobat", Toast.LENGTH_SHORT);
    }

    editText.setText("");
    contactosTextView.setText(contacte);
}

Eliminar contactes[modifica]

public void eliminarContacte(View view) {
    String idAEliminar = editText.getText().toString();
    long idEliminat = resolver.delete(CONTENT_URL, "id = ? ", new String[]{idAEliminar});

    editText.setText("");
    obtenirContactes();
}

Mostrar contactes[modifica]

public void mostrarContactes() {
    obtenirContactes():
}

public void obtenirContactes() {
    String[] dades = new String[]{"id", "name"};
    Cursor cursor = resolver.query(CONTENT_URL, dades, null, null, null);
    String llistaContactes = "";

    if(cursor.moveToFirst()) {
        do {
            String id = cursor.getString(cursor.getColumnIndex("id"));
            String nom = cursor.getString(cursor.getColumnIndex("name"));

            llistaContactes += id + " : " + nom + "\n";
        } while(cursor.moveToNext());
    }

    contactosTextView.setText(llistaContactes);
}

Captures de l'execució de les aplicacions[modifica]

Com podem veure a l'imatge trobem el ContentResolver amb un camp d'EditText on podem escriure un nom, un botó que afegirà aquest nom a la base de dades del ContentProvider, un altre botó per cercar noms mitjançant l'ID, un botó que serveix per eliminar noms mitjançant també l'ID i finalment un botó per mostrat tots els noms que tenim al ContenProvider.

  • Primera captura: Acció "MOSTRAR CONTACTOS"
  • Segona captura: Acció "BUSCAR CONTACTE" amb ID 33 al camp de l'EditText
  • Tercera captura: Acció "ELIMINAR CONTACTE" amb id 34 al camp de l'EditText

Referències[modifica]

Content Provider

Content Resolver

URI de contingut

SQLiteDatabase

<provider>