Content providers

It's time to further improve our application and introduce you to Android content providers. Content providers are one of the top power features Android Framework has to offer. What is the purpose of content providers? As its name suggests, content providers have the purpose of managing access to data stored by our application or stored by other applications. They provide a mechanism for sharing the data with other applications and provide a security mechanism for data access, that may or may not be from the same process.

Take a look at the following illustration displaying how content provider can manage access to shared storage:

We have a plan to share Notes and the TODOs data with other applications. Thanks to the abstraction layer content providers offers, it's easy to make the changes in the storage implementation layer without affecting the upper layers. Because of this, you can use content providers even if you do not plan to share any data with other applications. We can, for example, completely replace the persistence mechanism from SQLite to something completely different. Take a look at the following illustration showing this:

If you are not sure whether you need content provider or not, here is when you should implement it:

The Android Framework comes with an already defined content provider that you can use; for example, to manage contacts, audio, video, or other files. Content providers are not limited to SQLite access only, but you can use it for other structured data.

Let's once again highlight the main benefits:

So, as we already said, we are planning to support data exposure from the Journaler application. Before we create our content provider, we must note that this will require refactoring of the current code. Don't worry, we will present content provider, explain it to you and all the refactoring we do. After we do this--finish our implementation and refactoring--we will create an example client application that will use our content provider and trigger all the CRUD operations.

Let's create a ContentProvider class. Create a new package called provider with the JournalerProvider class extending the ContentProvider class.

Class beginning:

    package com.journaler.provider 
 
    import android.content.* 
    import android.database.Cursor 
    import android.net.Uri 
    import com.journaler.database.DbHelper 
    import android.content.ContentUris 
    import android.database.SQLException 
    import android.database.sqlite.SQLiteDatabase 
    import android.database.sqlite.SQLiteQueryBuilder 
    import android.text.TextUtils 
 
    class JournalerProvider : ContentProvider() { 
 
      private val version = 1 
      private val name = "journaler" 
      private val db: SQLiteDatabase by lazy { 
        DbHelper(name, version).writableDatabase 
    } 
 

Defining a companion object:

     companion object { 
        private val dataTypeNote = "note" 
        private val dataTypeNotes = "notes" 
        private val dataTypeTodo = "todo" 
        private val dataTypeTodos = "todos" 
        val AUTHORITY = "com.journaler.provider" 
        val URL_NOTE = "content://$AUTHORITY/$dataTypeNote" 
        val URL_TODO = "content://$AUTHORITY/$dataTypeTodo" 
        val URL_NOTES = "content://$AUTHORITY/$dataTypeNotes" 
        val URL_TODOS = "content://$AUTHORITY/$dataTypeTodos" 
        private val matcher = UriMatcher(UriMatcher.NO_MATCH) 
        private val NOTE_ALL = 1 
        private val NOTE_ITEM = 2 
        private val TODO_ALL = 3 
        private val TODO_ITEM = 4 
    } 
 

Class initialization:

    /** 
     * We register uri paths in the following format: 
     * 
     * <prefix>://<authority>/<data_type>/<id> 
     * <prefix> - This is always set to content:// 
     * <authority> - Name for the content provider 
     * <data_type> - The type of data we provide in this Uri 
     * <id> - Record ID. 
     */ 
    init { 
        /** 
         * The calls to addURI() go here, 
         * for all of the content URI patterns that the provider should
recognize. * * First: * * Sets the integer value for multiple rows in notes (TODOs) to
1. * Notice that no wildcard is used in the path. * * Second: * * Sets the code for a single row to 2. In this case, the "#"
wildcard is * used. "content://com.journaler.provider/note/3" matches, but * "content://com.journaler.provider/note doesn't. * * The same applies for TODOs. * * addUri() params: * * authority - String: the authority to match * * path - String: the path to match. * * may be used as a wild card for any text, * and # may be used as a wild card for numbers. * * code - int: the code that is returned when a
URI * is matched against the given components. */ matcher.addURI(AUTHORITY, dataTypeNote, NOTE_ALL) matcher.addURI(AUTHORITY, "$dataTypeNotes/#", NOTE_ITEM) matcher.addURI(AUTHORITY, dataTypeTodo, TODO_ALL) matcher.addURI(AUTHORITY, "$dataTypeTodos/#", TODO_ITEM) }

Overriding the onCreate() method:

     /** 
     * True - if the provider was successfully loaded 
     */ 
    override fun onCreate() = true 

Insert the operation as follows:

     override fun insert(uri: Uri?, values: ContentValues?): Uri { 
        uri?.let { 
            values?.let { 
                db.beginTransaction() 
                val (url, table) = getParameters(uri) 
                if (!TextUtils.isEmpty(table)) { 
                    val inserted = db.insert(table, null, values) 
                    val success = inserted > 0 
                    if (success) { 
                        db.setTransactionSuccessful() 
                    } 
                    db.endTransaction() 
                    if (success) { 
                        val resultUrl = ContentUris.withAppendedId
(Uri.parse(url), inserted) context.contentResolver.notifyChange(resultUrl,
null) return resultUrl } } else { throw SQLException("Insert failed, no table for
uri: " + uri) } } } throw SQLException("Insert failed: " + uri) }

Update the operation as follows:

     override fun update( 
            uri: Uri?, 
            values: ContentValues?, 
            where: String?, 
            whereArgs: Array<out String>? 
    ): Int { 
        uri?.let { 
            values?.let { 
                db.beginTransaction() 
                val (_, table) = getParameters(uri) 
                if (!TextUtils.isEmpty(table)) { 
                    val updated = db.update(table, values, where,
whereArgs) val success = updated > 0 if (success) { db.setTransactionSuccessful() } db.endTransaction() if (success) { context.contentResolver.notifyChange(uri, null) return updated } } else { throw SQLException("Update failed, no table for
uri: " + uri) } } } throw SQLException("Update failed: " + uri) }

Delete the operation as follows:

    override fun delete( 
            uri: Uri?, 
            selection: String?, 
            selectionArgs: Array<out String>? 
    ): Int { 
        uri?.let { 
            db.beginTransaction() 
            val (_, table) = getParameters(uri) 
            if (!TextUtils.isEmpty(table)) { 
                val count = db.delete(table, selection, selectionArgs) 
                val success = count > 0 
                if (success) { 
                    db.setTransactionSuccessful() 
                } 
                db.endTransaction() 
                if (success) { 
                    context.contentResolver.notifyChange(uri, null) 
                    return count 
                } 
            } else { 
                throw SQLException("Delete failed, no table for uri: "
+ uri) } } throw SQLException("Delete failed: " + uri) }

Performing query:

     override fun query( 
            uri: Uri?, 
            projection: Array<out String>?, 
            selection: String?, 
            selectionArgs: Array<out String>?, 
            sortOrder: String? 
     ): Cursor { 
        uri?.let { 
            val stb = SQLiteQueryBuilder() 
            val (_, table) = getParameters(uri) 
            stb.tables = table 
            stb.setProjectionMap(mutableMapOf<String, String>()) 
            val cursor = stb.query(db, projection, selection,
selectionArgs, null, null, null) // register to watch a content URI for changes cursor.setNotificationUri(context.contentResolver, uri) return cursor } throw SQLException("Query failed: " + uri) } /** * Return the MIME type corresponding to a content URI. */ override fun getType(p0: Uri?): String = when (matcher.match(p0)) { NOTE_ALL -> { "${ContentResolver.
CURSOR_DIR_BASE_TYPE}/vnd.com.journaler.note.items" } NOTE_ITEM -> { "${ContentResolver.
CURSOR_ITEM_BASE_TYPE}/vnd.com.journaler.note.item" } TODO_ALL -> { "${ContentResolver.
CURSOR_DIR_BASE_TYPE}/vnd.com.journaler.todo.items" } TODO_ITEM -> { "${ContentResolver.
CURSOR_ITEM_BASE_TYPE}/vnd.com.journaler.todo.item" } else -> throw IllegalArgumentException
("Unsupported Uri [ $p0 ]") }

Class ending:

     private fun getParameters(uri: Uri): Pair<String, String> { 
        if (uri.toString().startsWith(URL_NOTE)) { 
            return Pair(URL_NOTE, DbHelper.TABLE_NOTES) 
        } 
        if (uri.toString().startsWith(URL_NOTES)) { 
            return Pair(URL_NOTES, DbHelper.TABLE_NOTES) 
        } 
        if (uri.toString().startsWith(URL_TODO)) { 
            return Pair(URL_TODO, DbHelper.TABLE_TODOS) 
        } 
        if (uri.toString().startsWith(URL_TODOS)) { 
            return Pair(URL_TODOS, DbHelper.TABLE_TODOS) 
        } 
        return Pair("", "") 
       } 
 
     }  

Going from top to bottom, we did the following:

Now, when you have a content provider implementation, it is required to register it in your manifest as follows:

    <manifest xmlns:android=
"http://schemas.android.com/apk/res/android" package="com.journaler"> ... <application ... > ... <provider android:exported="true" android:name="com.journaler.provider.JournalerProvider" android:authorities="com.journaler.provider" /> ... </application> ... </manifest>

Observe. We set the exported attribute to True. What does this mean? It means that, if True, the Journaler provider is available to other applications. Any application can use the provider's content URI to access the data. One more important attribute is multiprocess. If the app runs in multiple processes, this attribute determines whether multiple instances of the Journaler provider are created. If True, each of the applications' processes has its own content provider instance.

Let's continue. In the Crud interface, add this to the companion object if you do not have it already:

    companion object { 
        val BROADCAST_ACTION = "com.journaler.broadcast.crud" 
        val BROADCAST_EXTRAS_KEY_CRUD_OPERATION_RESULT = "crud_result" 
   }  

We will rename our Db class into Content. Update the Content implementation, as follows, to use JournalerProvider:

    package com.journaler.database 
 
    import android.content.ContentValues 
    import android.location.Location 
    import android.net.Uri 
    import android.util.Log 
    import com.github.salomonbrys.kotson.fromJson 
    import com.google.gson.Gson 
    import com.journaler.Journaler 
    import com.journaler.model.* 
    import com.journaler.provider.JournalerProvider 
 
    object Content { 
 
      private val gson = Gson() 
      private val tag = "Content" 
 
      val NOTE = object : Crud<Note> { ... 
 

Note insert operation:

 
     ... 
     override fun insert(what: Note): Long { 
       val inserted = insert(listOf(what)) 
       if (!inserted.isEmpty()) return inserted[0] 
         return 0 
     } 
 
     override fun insert(what: Collection<Note>): List<Long> { 
        val ids = mutableListOf<Long>() 
        what.forEach { item -> 
           val values = ContentValues() 
           values.put(DbHelper.COLUMN_TITLE, item.title) 
           values.put(DbHelper.COLUMN_MESSAGE, item.message) 
           values.put(DbHelper.COLUMN_LOCATION,
gson.toJson(item.location)) val uri = Uri.parse(JournalerProvider.URL_NOTE) val ctx = Journaler.ctx ctx?.let { val result = ctx.contentResolver.insert(uri, values) result?.let { try { ids.add(result.lastPathSegment.toLong()) } catch (e: Exception) { Log.e(tag, "Error: $e") } } } } return ids } ...

Note update operation:

    .. 
    override fun update(what: Note) = update(listOf(what)) 
 
    override fun update(what: Collection<Note>): Int { 
      var count = 0 
      what.forEach { item -> 
          val values = ContentValues() 
          values.put(DbHelper.COLUMN_TITLE, item.title) 
          values.put(DbHelper.COLUMN_MESSAGE, item.message) 
          values.put(DbHelper.COLUMN_LOCATION,
gson.toJson(item.location)) val uri = Uri.parse(JournalerProvider.URL_NOTE) val ctx = Journaler.ctx ctx?.let { count += ctx.contentResolver.update( uri, values, "_id = ?", arrayOf(item.id.toString()) ) } } return count } ...

Note delete operation:

   ... 
   override fun delete(what: Note): Int = delete(listOf(what)) 
 
   override fun delete(what: Collection<Note>): Int { 
     var count = 0 
     what.forEach { item -> 
       val uri = Uri.parse(JournalerProvider.URL_NOTE) 
       val ctx = Journaler.ctx 
       ctx?.let { 
         count += ctx.contentResolver.delete( 
         uri, "_id = ?", arrayOf(item.id.toString()) 
       ) 
     } 
   } 
   return count 
  } ...  

Note select operation:

     ...  
     override fun select(args: Pair<String, String> 
      ): List<Note> = select(listOf(args)) 
 
     override fun select(args: Collection<Pair<String, String>>):  
List<Note> { val items = mutableListOf<Note>() val selection = StringBuilder() val selectionArgs = mutableListOf<String>() args.forEach { arg -> selection.append("${arg.first} == ?") selectionArgs.add(arg.second) } val ctx = Journaler.ctx ctx?.let { val uri = Uri.parse(JournalerProvider.URL_NOTES) val cursor = ctx.contentResolver.query( uri, null, selection.toString(),
selectionArgs.toTypedArray(), null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow(DbHelper.ID)) val titleIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_TITLE) val title = cursor.getString(titleIdx) val messageIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_MESSAGE) val message = cursor.getString(messageIdx) val locationIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_LOCATION) val locationJson = cursor.getString(locationIdx) val location = gson.fromJson<Location>
(locationJson) val note = Note(title, message, location) note.id = id items.add(note) } cursor.close() return items } return items } override fun selectAll(): List<Note> { val items = mutableListOf<Note>() val ctx = Journaler.ctx ctx?.let { val uri = Uri.parse(JournalerProvider.URL_NOTES) val cursor = ctx.contentResolver.query( uri, null, null, null, null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow(DbHelper.ID)) val titleIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_TITLE) val title = cursor.getString(titleIdx) val messageIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_MESSAGE) val message = cursor.getString(messageIdx) val locationIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_LOCATION) val locationJson = cursor.getString(locationIdx) val location = gson.fromJson<Location>
(locationJson) val note = Note(title, message, location) note.id = id items.add(note) } cursor.close() } return items } }

Todo object definition and its insert operation:

     ... 
     val TODO = object : Crud<Todo> { 
        override fun insert(what: Todo): Long { 
            val inserted = insert(listOf(what)) 
            if (!inserted.isEmpty()) return inserted[0] 
            return 0 
        } 
 
        override fun insert(what: Collection<Todo>): List<Long> { 
            val ids = mutableListOf<Long>() 
            what.forEach { item -> 
                val values = ContentValues() 
                values.put(DbHelper.COLUMN_TITLE, item.title) 
                values.put(DbHelper.COLUMN_MESSAGE, item.message) 
                values.put(DbHelper.COLUMN_LOCATION,
gson.toJson(item.location)) val uri = Uri.parse(JournalerProvider.URL_TODO) values.put(DbHelper.COLUMN_SCHEDULED,
item.scheduledFor) val ctx = Journaler.ctx ctx?.let { val result = ctx.contentResolver.insert(uri,
values) result?.let { try { ids.add(result.lastPathSegment.toLong()) } catch (e: Exception) { Log.e(tag, "Error: $e") } } } } return ids } ...

Todo update operation:

     ... 
     override fun update(what: Todo) = update(listOf(what)) 
 
     override fun update(what: Collection<Todo>): Int { 
        var count = 0 
        what.forEach { item -> 
                val values = ContentValues() 
                values.put(DbHelper.COLUMN_TITLE, item.title) 
                values.put(DbHelper.COLUMN_MESSAGE, item.message) 
                values.put(DbHelper.COLUMN_LOCATION,
gson.toJson(item.location)) val uri = Uri.parse(JournalerProvider.URL_TODO) values.put(DbHelper.COLUMN_SCHEDULED,
item.scheduledFor) val ctx = Journaler.ctx ctx?.let { count += ctx.contentResolver.update( uri, values, "_id = ?",
arrayOf(item.id.toString()) ) } } return count } ...

Todo delete operation:

     ... 
     override fun delete(what: Todo): Int = delete(listOf(what)) 
 
     override fun delete(what: Collection<Todo>): Int { 
            var count = 0 
            what.forEach { item -> 
                val uri = Uri.parse(JournalerProvider.URL_TODO) 
                val ctx = Journaler.ctx 
                ctx?.let { 
                    count += ctx.contentResolver.delete( 
                            uri, "_id = ?", arrayOf(item.id.toString()) 
                    ) 
                } 
            } 
            return count 
        } 

Todo select operation:

         ... 
        override fun select(args: Pair<String, String>): List<Todo> =  
select(listOf(args)) override fun select(args: Collection<Pair<String, String>>):
List<Todo> { val items = mutableListOf<Todo>() val selection = StringBuilder() val selectionArgs = mutableListOf<String>() args.forEach { arg -> selection.append("${arg.first} == ?") selectionArgs.add(arg.second) } val ctx = Journaler.ctx ctx?.let { val uri = Uri.parse(JournalerProvider.URL_TODOS) val cursor = ctx.contentResolver.query( uri, null, selection.toString(),
selectionArgs.toTypedArray(), null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow(DbHelper.ID)) val titleIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_TITLE) val title =
cursor.getString(titleIdx) val messageIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_MESSAGE) val message = cursor.getString(messageIdx) val locationIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_LOCATION) val locationJson = cursor.getString(locationIdx) val location = gson.fromJson<Location>
(locationJson) val scheduledForIdx = cursor.getColumnIndexOrThrow( DbHelper.COLUMN_SCHEDULED ) val scheduledFor = cursor.getLong(scheduledForIdx) val todo = Todo(title, message, location,
scheduledFor) todo.id = id items.add(todo) } cursor.close() } return items } override fun selectAll(): List<Todo> { val items = mutableListOf<Todo>() val ctx = Journaler.ctx ctx?.let { val uri = Uri.parse(JournalerProvider.URL_TODOS) val cursor = ctx.contentResolver.query( uri, null, null, null, null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow(DbHelper.ID)) val titleIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_TITLE) val title = cursor.getString(titleIdx) val messageIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_MESSAGE) val message = cursor.getString(messageIdx) val locationIdx = cursor.getColumnIndexOrThrow
(DbHelper.COLUMN_LOCATION) val locationJson = cursor.getString(locationIdx) val location = gson.fromJson<Location>
(locationJson) val scheduledForIdx = cursor.getColumnIndexOrThrow( DbHelper.COLUMN_SCHEDULED ) val scheduledFor = cursor.getLong(scheduledForIdx) val todo = Todo
(title, message, location, scheduledFor) todo.id = id items.add(todo) } cursor.close() } return items } } }

Read the code carefully. We replaced the direct database access with content provider. Update your UI classes to use the new refactored code. If you have trouble doing this, you can take a look at the GitHub branch containing these changes:

https://github.com/PacktPublishing/-Mastering-Android-Development-with-Kotlin/tree/examples/chapter_12.

The branch also contains an example of the Journaler content provider client application. We will highlight an example of use on the client application's main screen containing four buttons. Each button triggers an example of the CRUD operation, as shown here:

    package com.journaler.content_provider_client 
 
    import android.content.ContentValues 
    import android.location.Location 
    import android.net.Uri 
    import android.os.AsyncTask 
    import android.os.Bundle 
    import android.support.v7.app.AppCompatActivity 
    import android.util.Log 
    import com.github.salomonbrys.kotson.fromJson 
    import com.google.gson.Gson 
    import kotlinx.android.synthetic.main.activity_main.* 
 
   class MainActivity : AppCompatActivity() { 
 
     private val gson = Gson() 
     private val tag = "Main activity" 
 
     override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
 
        select.setOnClickListener { 
            val task = object : AsyncTask<Unit, Unit, Unit>() { 
                override fun doInBackground(vararg p0: Unit?) { 
                    val selection = StringBuilder() 
                    val selectionArgs = mutableListOf<String>() 
                    val uri = Uri.parse
("content://com.journaler.provider/notes") val cursor = contentResolver.query( uri, null, selection.toString(),
selectionArgs.toTypedArray(), null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow("_id")) val titleIdx = cursor.
getColumnIndexOrThrow("title") val title = cursor.getString(titleIdx) val messageIdx = cursor.
getColumnIndexOrThrow("message") val message = cursor.getString(messageIdx) val locationIdx = cursor.
getColumnIndexOrThrow("location") val locationJson = cursor.
getString(locationIdx) val location =
gson.fromJson<Location>(locationJson) Log.v( tag, "Note retrieved via content provider [
$id, $title, $message, $location ]" ) } cursor.close() } } task.execute() } insert.setOnClickListener { val task = object : AsyncTask<Unit, Unit, Unit>() { override fun doInBackground(vararg p0: Unit?) { for (x in 0..5) { val uri = Uri.parse
("content://com.journaler.provider/note") val values = ContentValues() values.put("title", "Title $x") values.put("message", "Message $x") val location = Location("stub location $x") location.latitude = x.toDouble() location.longitude = x.toDouble() values.put("location", gson.toJson(location)) if (contentResolver.insert(uri, values) !=
null) { Log.v( tag, "Note inserted [ $x ]" ) } else { Log.e( tag, "Note not inserted [ $x ]" ) } } } } task.execute() } update.setOnClickListener { val task = object : AsyncTask<Unit, Unit, Unit>() { override fun doInBackground(vararg p0: Unit?) { val selection = StringBuilder() val selectionArgs = mutableListOf<String>() val uri =
Uri.parse("content://com.journaler.provider/notes") val cursor = contentResolver.query( uri, null, selection.toString(),
selectionArgs.toTypedArray(), null ) while (cursor.moveToNext()) { val values = ContentValues() val id = cursor.getLong
(cursor.getColumnIndexOrThrow("_id")) val titleIdx =
cursor.getColumnIndexOrThrow("title") val title = "${cursor.getString(titleIdx)} upd:
${System.currentTimeMillis()}" val messageIdx =
cursor.getColumnIndexOrThrow("message") val message = "${cursor.getString(messageIdx)} upd:
${System.currentTimeMillis()}" val locationIdx =
cursor.getColumnIndexOrThrow("location") val locationJson =
cursor.getString(locationIdx) values.put("_id", id) values.put("title", title) values.put("message", message) values.put("location", locationJson) val updated = contentResolver.update( uri, values, "_id = ?",
arrayOf(id.toString()) ) if (updated > 0) { Log.v( tag, "Notes updated [ $updated ]" ) } else { Log.e( tag, "Notes not updated" ) } } cursor.close() } } task.execute() } delete.setOnClickListener { val task = object : AsyncTask<Unit, Unit, Unit>() { override fun doInBackground(vararg p0: Unit?) { val selection = StringBuilder() val selectionArgs = mutableListOf<String>() val uri = Uri.parse
("content://com.journaler.provider/notes") val cursor = contentResolver.query( uri, null, selection.toString(),
selectionArgs.toTypedArray(), null ) while (cursor.moveToNext()) { val id = cursor.getLong
(cursor.getColumnIndexOrThrow("_id")) val deleted = contentResolver.delete( uri, "_id = ?", arrayOf(id.toString()) ) if (deleted > 0) { Log.v( tag, "Notes deleted [ $deleted ]" ) } else { Log.e( tag, "Notes not deleted" ) } } cursor.close() } } task.execute() } } }

This example demonstrates how to trigger CRUD operations from other applications using content provider.