MediaStore what the ..? (part I -the theory about content provider)

Fares Othmane Mokhtari
8 min readJan 16, 2023

--

a few weeks ago I asked a bunch of people on different android spaces about the media store and whether it is tedious or not

once I saw the result, I made an assumption that many new developers who use only room are unable to understand all the different classes related to the old way of dealing with SQLite such as Cursor, and ContentValues so I decided to share some blog posts about these topics.

Media Store:

if you look in the official android documentation about the media store you see :

The contract between the media provider and applications. Contains definitions for the supported URIs and columns.

this single line has three keywords which are: contract, provider, and URI, and we encounter these words when we work with content providers so if we wanna understand the media store we should start from the beginning by asking this question what is a content provider?

Content Provider:

the content provider is one of the four components used in an android app

activities, services, receivers, and content providers.

content provider interacts with repositories so the client does not need to know from where or how the data is stored formatted or accessed, also I should mention that the content provider came in two flavors Stream and SqlLike, in these posts I’ll focus only on the SqlLike.

most of the time as developers we don’t build our content provider but we often deal with system providers such as MediaStore, CallLog, ContractContact … etc.

so it is better to get our hands dirty with content providers in order to understand how the system’s providers work.

to create a content provider we need first to extend the ContentProvider base class and override its methods.

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri

class MyContentProvider :ContentProvider()
{
override fun onCreate(): Boolean {
TODO("Not yet implemented")
}

override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
TODO("Not yet implemented")
}

override fun getType(uri: Uri): String? {
TODO("Not yet implemented")
}

override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
TODO("Not yet implemented")
}

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}

override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}
}

as you may notice these methods are old-school CRUD operations of SQlite in android.

let’s explain the parameters and the return types because they are what we are going to deal with when we consume content providers (such as datastore)

The Query:

the query method is a good starting point because it takes a bunch of parameters and returns a Cursor object

override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
TODO("Not yet implemented")
}

Uri:

Uri describes the domain of the data a requester is interested if we see it from an SQL point of view it is the table name, the official syntax of URI is

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

in any provider, we should provide the schema which is content://, and the host which is the authority, a symbolic name of the provider we gonna use it in the manifest when we declare the provider, both of them are mandatory for any URI, the last part is the path, from SQL point of view we can consider the path as table name, by convention developers offer access to a single row of the table by appending an ID at the end of the path, and we match this uri against some integers values using a UriMatcher.

the UriMatcher relies on wildcard characters to match the URI with integer values:

*: Matches a string of any valid characters of any length.

#: Matches a string of numeric characters of any length.

Projection:

from an SQL point of view this is a list of columns that should be included in the result.

Selection:

in many cases, we will use SQL like the selection string “name = ?”.

SelctionArgs:

it is an array of String values that should be included instead of the placeholder (?) used in the selection .

sortOrder:

we usually SQL like sort order this will usually be something like “name DESC

Cursors:

A cursor is an object that contains the result of the query, the Cursor interface has many functions that allow us to read and iterate over the result

the Cursor interface provides the following methods to manipulate its internal position:

boolean move(int offset); // Moves the position by the given offset
boolean moveToFirst(); // Moves the position to the first row
boolean moveToLast(); // Moves the position to the last row
boolean moveToNext(); /*Moves the cursor to the next row relative to the
current position*/
boolean moveToPosition(int position); /* Moves the cursor to the
specified position */
boolean moveToPrevious(); /* Moves the cursor to the
previous row relative to the current position*/

once the position is set using one of the above methods, we should use one of two methods to retrieve the column index

int getColumnIndex(String var1);
/*
this method takes a string parmater that indicate which column to read from,
if the column is not found it returns -1
*/

int getColumnIndexOrThrow(String var1) throws IllegalArgumentException;
/*
this method takes a string paramter that indicates which column to read from,
it throw an exception if the columns is not found
*/

once the index is retrieved we pass it to one of the following et methods

short getShort(int var1);
int getInt(int var1);
long getLong(int var1);
float getFloat(int var1);
double getDouble(int var1);
byte[] getBlob(int var1);

since the Cursor is an interface it is clear that it has at least one implementation in fact it has many implementations ready to use by us such as CrossProcessCursor,AbstractCursor, AbstractCursor, AbstractCursor, AbstractCursor, AbstractCursor, AbstractCursor, sqlite.sQLiteCursor.

Insert():

override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
TODO("Not yet implemented")
}

this method takes a Uri parameter and another interesting parameter which is the ContentValues?

ContentValues:

it is a maplike class that matches a value to a String key, it Contains multiple overload methods, each one of them takes a String key and a value, when we work with a Content provider using sqllike approach this key should correspond to the name of the column for the table that targeted by the insert .

   public void put(String key, String value) {
throw new RuntimeException("Stub!");
}
public void put(String key, Byte value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Short value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Integer value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Long value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Float value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Double value) {
throw new RuntimeException("Stub!");
}

public void put(String key, Boolean value) {
throw new RuntimeException("Stub!");
}

public void put(String key, byte[] value) {
throw new RuntimeException("Stub!");
}
public void putNull(String key) {
throw new RuntimeException("Stub!");
}

the insert method should return a Uri object that references the newly created row.

getType():

override fun getType(uri: Uri): String? {
TODO("Not yet implemented")
}

the getType method returns a String value which is MIME type for the given URI, when the URI refers to a table the returned String should start with vnd.android.cursor.dir/ if the URI to a single row the String value should start with vnd.android.cursor.item, after these prefixes that depend on the URI we should return the authority and the table from the URI for example if the URI is content://myAuthority/tableName/2023 the MIME would be vnd.android.cursor.item/myAuthority.tableName.

delete():

override fun delete(uri: Uri, selection: String?, 
selectionArgs: Array<out String>?): Int {
TODO("Not yet implemented")
}

the delete() method removes the row from the table that is specified by the Uri parameter could refer to a table or to the row of a table when the uri refers to a specific row we should use the selection and the selectionArgs parameters to determine which row(s) should be deleted.this method returns the number of the row that has been deleted.

the delete method should make a call to ContentResolver.notifyChange() to inform any observer that the table has been changed

update():

the update() method performs an update operation on the table specified by the URI

override fun update(uri: Uri,
contentValues: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?): Int

the contentValues parameter contains the updated column/value pairs for the table . the selection and selectionArgs parameter select which rows from the table the update should be applied to.

the update() method should make a call to ContentResolver.notifyChange() to notify observers that the table data has changed.

onCreate():

I let the onCreate method for the last because the idea of this blog post is to learn how to deal with system providers such as MediaStore but since I am planning to implement a content provider to deal with Room database in the next blog post before talking about MediaStore in the third one let’s discuss the onCreate().

override fun onCreate(): Boolean {
TODO("Not yet implemented")
}

the onCreate() method is called at the beginning of a content provider’s lifecycle and it is a convenient place to perform initialization, it returns a boolean to indicate if the initialization was successful (true) or not (false).

Registering the Content Provider:

once the content provider implementation is finished you must register it inside the Manifest.

<provider android:authorities="list"
android:directBootAware=["true" | "false"]
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:grantUriPermissions=["true" | "false"]
android:icon="drawable resource"
android:initOrder="integer"
android:label="string resource"
android:multiprocess=["true" | "false"]
android:name="string"
android:permission="string"
android:process="string"
android:readPermission="string"
android:syncable=["true" | "false"]
android:writePermission="string" >

</provider>

android:authorities: multiple authorities separated by a semicolon and to avoid conflicts we use java naming style such curta.dev.provider.myprovider , as mentioned above this authority is the same that we should to build our URI.

android:directBootAware: it indicates if the content provider can run before the user unlocks the device, the default is false .

android:enabled: it indicates if we can instantiate the content provider the default is true.

android:exported: whether the content provider is available for other application to use the default is false.

android:grantUriPermission: whether to give an application component one-time access to data protected by the readPermission, writePermission, permission, and exported attributes .

android:icon: An icon representing the content provider. This attribute must be set as a reference to a drawable resource containing the image definition. If it is not set, the icon specified for the application as a whole is used instead.

android:initOrder: an integer value that indicates the order in which the content provider should be instantiated compared to other content providers within the same process.

android:label: a user-readable label for the content provider.

android:multiprocess: if we should create a different instance for each process or a single content provider is shared across different process the default value is false

android:name: the name of the class that implements the content provider.

android:permission: the name of the permission that the client must have to read and write the content provider’s data .

android:process: the name of the process in which the content provider should run.

android:writePermission:a permission that client must have to make the change to the data controlled by the content provider

android:readPermission: permission that client mst have to query the content provider

android:syncable:whether or not the data under the content provider ‘s control is to be synchronized with data on a server

now we have enough theory to understand how the providers provided by the system work under the hood and we are able to build our content provider, in the next post we will talk about the requester (ContentResolver) and we will build our small content provider to deal with room database, story to get our hands dirty and the third one we will deal MediaStore

--

--