Lesson 101. Creating our ContentProvider

Lesson 101. Creating our ContentProvider


In this lesson:

– we create our ContentProvider

Content Provider is a way to share your usage data for the general public. This is usually database data. And creating an ISP class is like creating a regular database class. We use SQLiteOpenHelper to manage the database, and we implement the query, insert, update, delete methods of the ContentProvider class.

But there are also differences. Uri is used when working with the provider. It is a component and is similar to http. With Uri, the system understands which provider is needed, what data to work with, and what specific record. Uri can be represented as: content: // <authority> / <path> / <id>.

For example, take Uri – content: //ru.startandroid.provider.AdressBook/contacts/7 and break it apart:

content: // is the default start for the provider’s address.

startartroid.provider.AdressBook- this authority. Defines the provider (if you are analogous to the database, then this is the name of the database).

contacts are path. What data from the provider is required (table).

7 is it ID. What specific record is needed (record ID)

path can be a component, such as contacts / phones or contacts / email. This is used if the data structure is large enough and the data is stored in multiple tables according to some logic and organization.

ID may not be specified. This means that we will handle all records from path.

The Uri example above indicates to the system that we want to reach the address book provider ru.startandroid.provider.AdressBook, and access the contact with ID = 7.

Let’s try to create your own provider. Be it an address book – a contact list. We will only store two attributes for each contact: name and email.

And separately, we will create an application that will access and manipulate the data provider – read, add, modify, delete.

Let’s start with the provider. Let’s create a project without Activity:

Project name: P1011_ContentProvider
Build Target: Android 2.3.3
Application name: ContentProvider
Package name: ru.startandroid.develop.p1011contentprovider

Create a MyContactsProvider class that inherits android.content.ContentProvider

He suggests that we implement a bunch of methods. We are implementing.

MyContactsProvider.java:

package ru.startandroid.develop.p1011contentprovider;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log; 

public class MyContactsProvider extends ContentProvider {
  final String LOG_TAG = "myLogs";

  // // Константы для БД
  // БД
  static final String DB_NAME = "mydb";
  static final int DB_VERSION = 1;

  // Таблица
  static final String CONTACT_TABLE = "contacts";

  // Поля
  static final String CONTACT_ID = "_id";
  static final String CONTACT_NAME = "name";
  static final String CONTACT_EMAIL = "email";

  // Скрипт создания таблицы
  static final String DB_CREATE = "create table " + CONTACT_TABLE + "("
      + CONTACT_ID + " integer primary key autoincrement, "
      + CONTACT_NAME + " text, " + CONTACT_EMAIL + " text" + ");";

  // // Uri
  // authority
  static final String AUTHORITY = "ru.startandroid.providers.AdressBook";

  // path
  static final String CONTACT_PATH = "contacts";

  // Общий Uri
  public static final Uri CONTACT_CONTENT_URI = Uri.parse("content://"
      + AUTHORITY + "https://startandroid.ru/" + CONTACT_PATH);

  // Типы данных
  // набор строк
  static final String CONTACT_CONTENT_TYPE = "vnd.android.cursor.dir/vnd."
      + AUTHORITY + "." + CONTACT_PATH;

  // одна строка
  static final String CONTACT_CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd."
      + AUTHORITY + "." + CONTACT_PATH;

  //// UriMatcher
  // общий Uri
  static final int URI_CONTACTS = 1;

  // Uri с указанным ID
  static final int URI_CONTACTS_ID = 2;

  // описание и создание UriMatcher
  private static final UriMatcher uriMatcher;
  static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, CONTACT_PATH, URI_CONTACTS);
    uriMatcher.addURI(AUTHORITY, CONTACT_PATH + "/#", URI_CONTACTS_ID);
  }

  DBHelper dbHelper;
  SQLiteDatabase db;

  public boolean onCreate() {
    Log.d(LOG_TAG, "onCreate");
    dbHelper = new DBHelper(getContext());
    return true;
  }

  // чтение
  public Cursor query(Uri uri, String[] projection, String selection,
      String[] selectionArgs, String sortOrder) {
    Log.d(LOG_TAG, "query, " + uri.toString());
    // проверяем Uri
    switch (uriMatcher.match(uri)) {
    case URI_CONTACTS: // общий Uri
      Log.d(LOG_TAG, "URI_CONTACTS");
      // если сортировка не указана, ставим свою - по имени
      if (TextUtils.isEmpty(sortOrder)) {
        sortOrder = CONTACT_NAME + " ASC";
      }
      break;
    case URI_CONTACTS_ID: // Uri с ID
      String id = uri.getLastPathSegment();
      Log.d(LOG_TAG, "URI_CONTACTS_ID, " + id);
      // добавляем ID к условию выборки
      if (TextUtils.isEmpty(selection)) {
        selection = CONTACT_ID + " = " + id;
      } else {
        selection = selection + " AND " + CONTACT_ID + " = " + id;
      }
      break;
    default:
      throw new IllegalArgumentException("Wrong URI: " + uri);
    }
    db = dbHelper.getWritableDatabase();
    Cursor cursor = db.query(CONTACT_TABLE, projection, selection,
        selectionArgs, null, null, sortOrder);
    // просим ContentResolver уведомлять этот курсор 
    // об изменениях данных в CONTACT_CONTENT_URI
    cursor.setNotificationUri(getContext().getContentResolver(),
        CONTACT_CONTENT_URI);
    return cursor;
  }

  public Uri insert(Uri uri, ContentValues values) {
    Log.d(LOG_TAG, "insert, " + uri.toString());
    if (uriMatcher.match(uri) != URI_CONTACTS)
      throw new IllegalArgumentException("Wrong URI: " + uri);

    db = dbHelper.getWritableDatabase();
    long rowID = db.insert(CONTACT_TABLE, null, values);
    Uri resultUri = ContentUris.withAppendedId(CONTACT_CONTENT_URI, rowID);
    // уведомляем ContentResolver, что данные по адресу resultUri изменились
    getContext().getContentResolver().notifyChange(resultUri, null);
    return resultUri;
  }

  public int delete(Uri uri, String selection, String[] selectionArgs) {
    Log.d(LOG_TAG, "delete, " + uri.toString());
    switch (uriMatcher.match(uri)) {
    case URI_CONTACTS:
      Log.d(LOG_TAG, "URI_CONTACTS");
      break;
    case URI_CONTACTS_ID:
      String id = uri.getLastPathSegment();
      Log.d(LOG_TAG, "URI_CONTACTS_ID, " + id);
      if (TextUtils.isEmpty(selection)) {
        selection = CONTACT_ID + " = " + id;
      } else {
        selection = selection + " AND " + CONTACT_ID + " = " + id;
      }
      break;
    default:
      throw new IllegalArgumentException("Wrong URI: " + uri);
    }
    db = dbHelper.getWritableDatabase();
    int cnt = db.delete(CONTACT_TABLE, selection, selectionArgs);
    getContext().getContentResolver().notifyChange(uri, null);
    return cnt;
  }

  public int update(Uri uri, ContentValues values, String selection,
      String[] selectionArgs) {
    Log.d(LOG_TAG, "update, " + uri.toString());
    switch (uriMatcher.match(uri)) {
    case URI_CONTACTS:
      Log.d(LOG_TAG, "URI_CONTACTS");

      break;
    case URI_CONTACTS_ID:
      String id = uri.getLastPathSegment();
      Log.d(LOG_TAG, "URI_CONTACTS_ID, " + id);
      if (TextUtils.isEmpty(selection)) {
        selection = CONTACT_ID + " = " + id;
      } else {
        selection = selection + " AND " + CONTACT_ID + " = " + id;
      }
      break;
    default:
      throw new IllegalArgumentException("Wrong URI: " + uri);
    }
    db = dbHelper.getWritableDatabase();
    int cnt = db.update(CONTACT_TABLE, values, selection, selectionArgs);
    getContext().getContentResolver().notifyChange(uri, null);
    return cnt;
  }

  public String getType(Uri uri) {
    Log.d(LOG_TAG, "getType, " + uri.toString());
    switch (uriMatcher.match(uri)) {
    case URI_CONTACTS:
      return CONTACT_CONTENT_TYPE;
    case URI_CONTACTS_ID:
      return CONTACT_CONTENT_ITEM_TYPE;
    }
    return null;
  }

  private class DBHelper extends SQLiteOpenHelper {

    public DBHelper(Context context) {
      super(context, DB_NAME, null, DB_VERSION);
    }

    public void onCreate(SQLiteDatabase db) {
      db.execSQL(DB_CREATE);
      ContentValues cv = new ContentValues();
      for (int i = 1; i <= 3; i++) {
        cv.put(CONTACT_NAME, "name " + i);
        cv.put(CONTACT_EMAIL, "email " + i);
        db.insert(CONTACT_TABLE, null, cv);
      }
    }

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
  }
}

There is a lot of code, but there is practically nothing new for us. Basically work with the database.

At the beginning is a bunch of constants. DB constants must be familiar and understandable in the past lessons, I do not explain them. I will only explain that in our database there will be only one table contacts with three fields: _id, name and email.

Next are the constants AUTHORITY and CONTACT_PATH are Uri components. We already discussed this at the beginning of the lesson. From these two constants and the content prefix: // we form a common Uri - CONTACT_CONTENT_URI. Since no ID is specified here, this Uri gives access to all contacts.

We got here that the name of the table in the database coincided with path in Uri. It is not necessary, they can be different.

hereinafter described MIME types data provided by the provider. One for the dataset, the other for the particular record. I have no experience with ISPs yet and I don't really understand where and how these types of data can be used. But we need to implement them, so we do it. We will return them in our provider's getType method.

Next, we create and describe the UriMatcher and its constants. UriMatcher is a parser type. In the addURI method, we give it a combination of: authority, path and constant. And, we can use special characters: * - a string of any character of any length, # - a string of numbers of any length. Uri will be coming to the ISP's input and we will submit them to UriMatcher for verification. If Uri will fit the combination authority and pathPreviously added to addURI, the UriMatcher will return constant from the same set: authority, path, constant.

That is, the line:

uriMatcher.addURI (AUTHORITY, CONTACT_PATH, URI_CONTACTS);

means that we have added a combination of AUTHORITY, CONTACT_PATH, and uriMatcher to URI_CONTACTS.

And the line

uriMatcher.addURI (AUTHORITY, CONTACT_PATH + "/ #", URI_CONTACTS_ID);

means that we have added a combination of AUTHORITY, CONTACT_PATH + "/ #" to uriMatcher, and URI_CONTACTS_ID. # is a mask for a string of numbers. And if we have a number attached to the path, it means - they give us an ID and we will work with a specific record.

And now, if we ask uriMatcher to check for Uri consisting of AUTHORITY and CONTACT_PATH, it will return the value to us URI_CONTACTS. And if we give him a Uri consisting of AUTHORITY, CONTACT_PATH, and a number (ID), he will return us URI_CONTACTS_ID. And by these constants we will define - to work with all records or any specific.

In general, in words everything is very difficult, it will be easier in the code. But the main point of this uriMatcher is that it will determine which Uri came to us - shared or with ID. If common - then returns URI_CONTACTS, if with ID - then returns URI_CONTACTS_ID.

Let's disassemble the methods.

IN OnCreate we create DBHelper - already familiar to us the assistant for work with a DB.

In the method query we get the Uri input and a set of parameters to sample from the database: projection - columns, selection - condition, selectionArgs - arguments for condition, sortOrder - sorting. Again, these parameters are already familiar to us in working with the database.

Next, we give uri to the match method of the uriMatcher object. It parses it, verifies it with those authority / path combinations we gave it in addURI methods, and outputs a constant from the corresponding combination. If this is URI_CONTACTS, then the total Uri has come to us and the provider wants to get all his records. In this case, we will check that the sort is specified. If not, we'll put sort by name. As you understand, this sorting operation is absolutely optional. We could do nothing. If we received the URI_CONTACTS_ID, then the provider must return the record by a specific ID. To do this, we extract the ID from the Uri method with the getLastPathSegment method and add it to the selection condition.

If uriMatcher failed to recognize Uri, then we will issue an IllegalArgumentException. Of course, you can list your solution here.

Then we get the database and do the query method for it, we get the cursor. We are registering this cursor to receive a notification when the data corresponding to the total Uri is changed - CONTACT_CONTENT_URI. Changing any particular entry will also trigger the message. At the end we return the cursor.

IN insert we check that our total Uri has arrived. If everything is ok then we insert the data into the table and get the ID. We add this ID to the total Uri and get the Uri with the ID. Ideally, this can also be done using normal line drawing, but it is recommended to use the withAppendedId object method. We next notify the system that changed the data corresponding to resultUri. The system will see if there are no listeners on this Uri. He will see that we have registered the cursor and will let him know that the data has been updated. At the end, we return the resultUri corresponding to the newly added record.

IN delete we check which Uri came to us. If with ID, then fix selection - we add a condition there by ID. We delete the database, we get the number of deleted records. We are writing to inform you that the data has changed. Return the number of deleted records.

IN update we check which Uri came to us. If with ID, then fix selection - we add a condition there by ID. We update the database, get the number of updated records. We are writing to inform you that the data has changed. Return the number of updated records.

In the method getType return types according to type Uri - general or with ID.

class DBHelper helps us create a database and populate it with raw data. The update is not implemented here.

It remains to spell out the ISP class in the manifesto. This is done the same way we prescribe Activity as a gas company. Only from the list we choose Provider. At Name we choose our class. And fill the field Authorities, The value here must be specified with AUTHORITY constant - ru.startandroid.providers.AdressBook.

Now, when the system receives a request for Uri data from authority = ru.startandroid.providers.AdressBook, it will work with our provider.

With the provider everything. It can be installed on AVD. This is done as usual, just nothing will appear on the screen because there is no Activity. And the console will have approximately the following lines:

Uploading P1011_ContentProvider.apk onto device 'emulator-5554'
Installing P1011_ContentProvider.apk ...
Success!
P1011_ContentProvider bin P1011_ContentProvider.apk installed on device
Done!

We are now writing an application that will contact the provider. Let's create a project:

Project name: P1012_ContProvClient
Build Target: Android 2.3.3
Application name: ContProvClient
Package name: ru.startandroid.develop.p1012contprovclient
Create Activity: MainActivity

Add to strings.xml rows:

Insert
Update
Delete
Error

screen main.xml:



	
		
		
		
		
	
	
	

4 buttons for data operations and a list for data provider.

MainActivity.java:

package ru.startandroid.develop.p1012contprovclient;

import android.app.Activity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;

public class MainActivity extends Activity {
  
  final String LOG_TAG = "myLogs";

  final Uri CONTACT_URI = Uri
      .parse("content://ru.startandroid.providers.AdressBook/contacts");
  
  final String CONTACT_NAME = "name";
  final String CONTACT_EMAIL = "email";

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Cursor cursor = getContentResolver().query(CONTACT_URI, null, null,
        null, null);
    startManagingCursor(cursor);

    String from[] = { "name", "email" };
    int to[] = { android.R.id.text1, android.R.id.text2 };
    SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
        android.R.layout.simple_list_item_2, cursor, from, to);

    ListView lvContact = (ListView) findViewById(R.id.lvContact);
    lvContact.setAdapter(adapter);
  }
  
  public void onClickInsert(View v) {
        ContentValues cv = new ContentValues();
        cv.put(CONTACT_NAME, "name 4");
        cv.put(CONTACT_EMAIL, "email 4");
        Uri newUri = getContentResolver().insert(CONTACT_URI, cv);
        Log.d(LOG_TAG, "insert, result Uri : " + newUri.toString());
  }

  public void onClickUpdate(View v) {
        ContentValues cv = new ContentValues();
        cv.put(CONTACT_NAME, "name 5");
        cv.put(CONTACT_EMAIL, "email 5");
        Uri uri = ContentUris.withAppendedId(CONTACT_URI, 2);
        int cnt = getContentResolver().update(uri, cv, null, null);
        Log.d(LOG_TAG, "update, count = " + cnt);
  }

  public void onClickDelete(View v) {
    Uri uri = ContentUris.withAppendedId(CONTACT_URI, 3);
        int cnt = getContentResolver().delete(uri, null, null);
        Log.d(LOG_TAG, "delete, count = " + cnt);
  }

  public void onClickError(View v) {
    Uri uri = Uri.parse("content://ru.startandroid.providers.AdressBook/phones");
    try { 
      Cursor cursor = getContentResolver().query(uri, null, null, null, null);
    } catch (Exception ex) {
      Log.d(LOG_TAG, "Error: " + ex.getClass() + ", " + ex.getMessage());
    }
    
  }
}

IN CONTACT_URI we store the total Uri. IN CONTACT_NAME and CONTACT_EMAIL - field names.

IN onCreate we use the getContentResolver method to get ContentResolver. This object is a mediator between us and the provider. We call it the query method and pass Uri there. Other options are left blank - that is, we will return all entries, we do not specify all fields and sorts. We pass the received cursor to Activity for management - method startManagingCursor. Next, we create an adapter and assign it to a list.

IN onClickInsert we use the insert method to add records to the provider. This method returns us the Uri corresponding to the new entry.

IN onClickUpdate we create Uri corresponding record with ID = 2 and update this record with the provider.

IN onClickDelete we create a Uri corresponding record with ID = 3 and delete that record from the provider.

IN onClickError we are trying to get records by Uri that is not known by the provider. In his uriMatcher did not add information about this Uri. In this case, we generated an error from the provider. Here we will try to catch her.

We all save and launch the application.

In the logs:

onCreate
query, content: //startandroid.providers.AdressBook/contacts
URI_CONTACTS

Provider created. His query method was executed and he was logged in to Uri - content: //startandroid.providers.AdressBook/contacts. uriMatcher returned URI_CONTACTSThat is, Uri recognized - as a general, the requester of all data. We ended up with a cursor with all the data and listed it.

press Insert. A new line appears in the list.

It should be noted here that we did not even touch the cursor, adapter, or list. We just added a record to the provider, and our list has updated itself. This is the message we wrote in the provider's methods. The cursor is waiting for a message to update the provider, and the insert method sent him such a message.

List:

insert, content: //startandroid.providers.AdressBook/contacts
insert, result Hours: content: //startandroid.providers.AdressBook/contacts/4

the method was executed insert, I received a common Uri, which indicated which table to insert the data into. Data added and provider returned Uri new entry: content: //startandroid.providers.AdressBook/contacts/4

press Update.

We updated the second entry, and it went to the end of the list, because we have sorted the name of the provider by name, unless otherwise stated.

List:

update, content: //startandroid.providers.AdressBook/contacts/2
URI_CONTACTS_ID, 2
update, count = 1

the method worked update, Got the input Uri = content: //startandroid.providers.AdressBook/contacts/2. UriMatcher correctly recognized that the received Uri contained an ID. The provider updated the record and returned the number of updated records to us.

Click Delete.

Deleted record with ID = 3.

List:

delete, content: //startandroid.providers.AdressBook/contacts/3
URI_CONTACTS_ID, 3
delete, count = 1

the method worked delete, Got the input Uri = content: //startandroid.providers.AdressBook/contacts/3. UriMatcher has determined that Uri has an ID. The title was removed and we received a number of deleted records.

press Error.

List:

query, content: //startandroid.providers.AdressBook/phones
Error: class java.lang.IllegalArgumentException, Wrong URI: content: //startandroid.providers.AdressBook/phones

The query method with Uri = was executed content: //startandroid.providers.AdressBook/phones. But UriMatcher doesn't know the combination authority (Ru.startandroid.providers.AdressBook) and path (Phones). In this situation, we configured the provider to generate an error. In the appendix, we catch this error and log it.

There are some points that I would like to point out separately.

managedQuery

We used query and startManagingCursor methods in the application. Activity has a method that combines these two methods - managedQuery. It inputs the same parameters as query and returns a cursor that is already running Activity.

provider constants

We created constants in the application and put values ​​from the provider there. It came out hardcode. And it would be more correct to use these constants directly from the ISP class. For this purpose it is possible to allocate all necessary constants in a separate class, create a .jar library from it and distribute it. Developers will add it to their project and from there they can use all the constants they need to work with the provider.

getWritableDatabase

The getWritableDatabase method for performance reasons is not recommended to call the onCreate method of the provider. Therefore, onCreate we only created DBHelper, and in the methods of query, insert and others called getWritableDatabase () and accessed the database.

sampling conditions

I did not use sampling and sorting options when working with a provider in this lesson. They are quite similar to what we did in SQLite lessons. Don't forget about them.

More information about this can be found on the official website.

In the next lesson:

- handle the touch




Discuss in the forum [81 replies]

Leave a Comment