• 8 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 4/27/23

Expose your data with a content provider

Why should you expose your data?

Now that the app is functional, it would be interesting to securely expose the contents of its SQLite database to other apps than ours.

Alright, but I don’t quite understand why we’d need to? 

Well, sometimes an Android application may publicly and securely offer access to some of its data. For example, imagine that a company such as Tripadvisor or Booking.com contacts you and offers you a paid partnership to view the list of things to do for each of your users, in order to offer them discounts and deals via their mobile apps: You would then need to provide a secure, limited access to your SQLite database! 🙂

This exposure is possible on Android via a content provider, allowing you to share content that you previously defined with applications other than your own.

Overview of how a Content Provider works
Overview of how a content provider works

In a previous chapter, you saw how to publicly expose a file using the class FileProvider, inheriting from the class ContentProvider. This time, to expose our SQLite database, we will directly use the class ContentProvider. 🙂 Once it has been configured, outside apps will access the exposed data by providing it with a URI that it will analyze to return the appropriate data.

Start by creating the package provider/ in which you’ll create the class ItemContentProvider.

Class provider/ItemContentProvider.kt
class ItemContentProvider : ContentProvider() {
 
 
   override fun onCreate(): Boolean {
       return true
   }

   override fun query(uri: Uri, projection: Array<String>?,
                       selection: String?,
                       selectionArgs: Array<String>?,
                       sortOrder: String?): Cursor? {      return null;
   }
   
   override fun getType(uri: Uri): String? {
      return null;
   }
   
   override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
      return null;
   }
 
   override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
      return 0;
   }

   override fun update(uri: Uri,
              contentValues: ContentValues?,
              s: String?,
              strings: Array<String>?): Int {     
       return 0;
   }
}

Here we’ve created a class, ItemContentProvider, inheriting from ContentProvider, whose purpose will be to expose the data of our SQLite database. A class inheriting from ContentProvider must implement the following six methods:

  • onCreate(): Represents the content provider’s entry point. This way, you’ll be able to initialize different variables that will be useful later on.

  • query(): This method will take a URI as input, and retrieve the data (via a Cursor) from the destination of your choice (in our case, our SQLite database).

  • getType(): This method allows you to return the type MIME associated with the URI to more accurately identify the type of returned data.

  • insert(): This method will take a URI as input and insert data in ContentValues format into the destination of your choice (in our case, our SQLite database).

  • delete(): This method will take a URI as input and delete data in ContentValues format from the destination of your choice (in our case, our SQLite database).

  • update():  This method will take a URI as input and update data in ContentValues format in the destination of your choice (in our case, our SQLite database).

For now, these methods are empty. Don’t worry, we’ll be filling them in good time! 🙂 First, we need to declare our content provider in the manifest of our application to activate it.

Excerpt from AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>

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

Using the same principle as we did with FileProvider in a previous chapter, we must declare the content provider in the manifest of our application to make it accessible to other applications.

Now that content provider has been correctly declared, we will be able to configure it and give it access to our SQLite database - and our item table. To do so, we will slightly edit the DAO managing our Item table to be able to return an object of the type Cursor, which can more easily be manipulated by the content provider.

Excerpt from ItemDao.java
@Dao
interface ItemDao {
 
   @Query("SELECT * FROM Item WHERE userId = :userId")
   fun getItemsWithCursor(userId: Long): Cursor

   ...
}

Next, we’ll edit our template class item to create a public method, enabling it to turn a ContentValues object into an item object.

Excerpt from Item.kt
class Item {

   ...
   
   companion object {
 
       // --- UTILS ---
       fun fromContentValues(values: ContentValues): Item {
            val item = Item()
            if (values.containsKey("text")) item.text = values.getAsString("text")
            if (values.containsKey("category")) item.category = values.getAsInteger("category")
            if (values.containsKey("isSelected")) item.selected = values.getAsBoolean("isSelected")
            if (values.containsKey("userId")) item.userId = values.getAsLong("userId")
            return item
       }
   }
}

The class ContentValues is a basic dictionary that allows us to retrieve a value from a key. 🙂 We will be placing this value into the corresponding property of the item object. Now let’s edit our content provider to expose our item table.

Class ItemContentProvider.kt
class ItemContentProvider : ContentProvider() {
 
    companion object {
       // FOR DATA
       val AUTHORITY = "com.openclassrooms.savemytrip.provider"
       val TABLE_NAME = Item::class.java.simpleName
       val URI_ITEM = Uri.parse("content://$AUTHORITY/$TABLE_NAME")
   }
 
   override fun onCreate(): Boolean {
       return true
   }
 
   override fun query(uri: Uri, projection: Array<String>?,
                       selection: String?,
                       selectionArgs: Array<String>?,
                       sortOrder: String?): Cursor? {
 
       if (context != null) {
            val userId = ContentUris.parseId(uri)
            val db = SaveMyTripDatabase.getInstance(context)
            val cursor = db!!.itemDao().getItemsWithCursor(userId)
            cursor.setNotificationUri(context.contentResolver, uri)
            return cursor
       }
 
        throw IllegalArgumentException("Failed to query row for uri $uri")
   }
 
   override fun getType(uri: Uri): String? {
       return "vnd.android.cursor.item/$AUTHORITY.$TABLE_NAME"
   }
 
    override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
 
       if (context != null) {
           val db = SaveMyTripDatabase.getInstance(context)
           val id = db!!.itemDao().insertItem(Item.fromContentValues(contentValues!!))
           if (id != 0L) {
               context.contentResolver.notifyChange(uri, null)
               return ContentUris.withAppendedId(uri, id)
           }
       }
 
       throw IllegalArgumentException("Failed to insert row into $uri")
   }
 
   override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
       if (context != null) {
           val db = SaveMyTripDatabase.getInstance(context)
           val count = db!!.itemDao().deleteItem(ContentUris.parseId(uri))
           context.contentResolver.notifyChange(uri, null)
           return count
       }
       throw IllegalArgumentException("Failed to delete row into $uri")
   }
 
   override fun update(uri: Uri, contentValues: ContentValues?, s: String?, strings: Array<String>?): Int {
       if (context != null) {
            val db = SaveMyTripDatabase.getInstance(context)
            val count = db!!.itemDao().updateItem(Item.fromContentValues(contentValues!!))
            context.contentResolver.notifyChange(uri, null)
            return count
       }
       throw IllegalArgumentException("Failed to update row into $uri")
   }
    
}

Here we have completed all the methods of our content provider. First, we declared some value variables in a companion object that help us identify the authority defining our content provider, the name of the table that we will query, and the base URI that will need to communicate with it.

In general, you'll notice that we follow a certain logic for each of the methods of our content provider:

  1. First, we conduct an operation (create, retrieve, update, and delete) on our SQLite database, and more particularly on the item table, using our SaveMyTripDatabase object that will call the DAO ItemDao.

  2. Second, we resend or update the URI of the manipulated resource to inform the user that the operation was a success.

For the methods  update  and  delete, we return the number of lines that were impacted by the operation in question.

At the end of each of our methods, we return an exception IllegalArgumentException if at any point the content provider cannot fully complete the operation.

Alright! But still, I wonder how we’re going to test our content provider... I’ll need to create a second app, right? 🙂

There is a simpler way to achieve this - particularly with instrumented tests! 😁 To do so, I created a test class for you, ItemContentProviderTest, which will enable you to check if our content provider is working as it should.

Class ItemContentProviderTest.kt
@RunWith(AndroidJUnit4::class)
class ItemContentProviderTest {
 
   // FOR DATA
   private var mContentResolver: ContentResolver? = null
 
   @Before
   fun setUp() {
       Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
               SaveMyTripDatabase::class.java)
               .allowMainThreadQueries()
               .build()
       mContentResolver = InstrumentationRegistry.getContext().contentResolver
   }
 
   @Test
   fun getItemsWhenNoItemInserted() {
       val cursor = mContentResolver!!.query(ContentUris.withAppendedId(ItemContentProvider.URI_ITEM, USER_ID), null, null, null, null)
       assertThat(cursor, notNullValue())
       assertThat(cursor!!.count, `is`(0))
       cursor.close()
   }
 
   @Test
   fun insertAndGetItem() {
       // BEFORE : Adding demo item
       val userUri = mContentResolver!!.insert(ItemContentProvider.URI_ITEM, generateItem())
       // TEST
       val cursor = mContentResolver!!.query(ContentUris.withAppendedId(ItemContentProvider.URI_ITEM, USER_ID), null, null, null, null)
       assertThat(cursor, notNullValue())
       assertThat(cursor!!.count, `is`(1))
       assertThat(cursor.moveToFirst(), `is`(true))
       assertThat(cursor.getString(cursor.getColumnIndexOrThrow("text")), `is`("Visite cet endroit de rêve !"))
   }
 
   // ---
 
   private fun generateItem(): ContentValues {
        val values = ContentValues()
        values.put("text", "Visite cet endroit de rêve !")
        values.put("category", "0")
        values.put("isSelected", "false")
        values.put("userId", "1")
        return values
   }
 
   companion object {
 
       // DATA SET FOR TEST
       private val USER_ID: Long = 1
   }
}

I've created an instrumented test that will play the role of the outside app wishing to access our application’s data. For this, I used the class ContentResolver, taking a URI as input to communicate with our content provider by calling its various methods.

Now let’s run the test. Once it’s done, if you start the app, you should see that a new thing to do has been added to the list... by your test... via the content provider! 🙂

Pretty neat, isn't it? Our test played the role of the outside app and has added real data within our SQLite database!

Let's recap!

  • Expose your data using a content provider.

  • There are six methods class inheriting from a content provider must implement:

    • onCreate()

    • query()

    • getType()

    • insert()

    • delete()

    • update()

Additionally, be aware that projects which implement the  content provider (or to be specific, that need to expose their data) are quite rare; in particular because of how complex this class is to use. But all the same, it’s important to see it at least once. 😉

Example of certificate of achievement
Example of certificate of achievement