Matthew Tyler

Testing Content Providers Android Programming

Content Providers are a means of encapsulating and providing data to applications through a simple interface. Content Providers are therefore one of the main building blocks in android applications along with services and activities.

Now, you can quite easily embed raw sql queries and data access methods directly into your activity, but it is cleaner and better practice to have some separate interface that you can use isolate and test data access methods. The Content Provider is also required if you want to expose data in your application to third-party applications. Imagine you wanted to implemented a widget that uses the data from your application; it would require you to define a Content Provider to do so.

In order to define a Content Provider, you must implement the following methods;

  • onCreate() which is called to initialize the provider
  • query(Uri, String[], String, String[], String) which returns data to the caller
  • insert(Uri, ContentValues) which inserts new data into the content provider
  • update(Uri, ContentValues, String, String[]) which updates existing data in the content provider
  • delete(Uri, String, String[]) which deletes data from the content provider
  • getType(Uri) which returns the MIME type of data in the content provider

It is important to note, that the primary mechanism by which we are making queries is through the Uri. Every Content Provider defines an authority (usually the package name with that of Content Provider appended to it ie: com.example.myapp.MyContentProvider) which is declared in your applications manifest file, within the application tag, like thus;

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.INTERNET" />
    
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Workspace"
            android:label="@string/title_activity_workspace" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider     android:name="com.flashics.sutra.AsanaProvider" 
                android:authorities="com.example.myapp.MyContentProvider"/>  
    </application>
</manifest>

The next step in building a Content Provider is to identify what data needs to be queried. For the sake of brevity, we will implement a content provider that will can query the asana api to return a list of workspaces, and a list of projects in a particular workspace. This link contains the asana api documentation.

From this documentation, we can see that the two http requests that need to be made are;

https://app.asana.com/api/1.0/workspaces

and

https://app.asana.com/api/1.0/workspaces/x/projects where x is the id of the workspace

For actually making the requests, I will use the aquery library for simplicity, which can be found here - though you should be able to follow along without knowledge of this library.

Content Providers function by being passed a Uri which determines what data it should query. You then need to determine a sensible set of Uri's for determining what data to fetch.

Usually, the uri should be of the format content://<authority>/tablename so in our example, we will define our two uri's as

content://com.example.myapp/workspaces

and

content://com.example/myapp/projects/workspaces/x where x is the id of the workspace

Note that I've rearranged the uri a little as compared with the http request. The URI object libraries in the android/java api have helper functions that make it easy to strip the last path segment.

Now onto the actual content provider code!

We first define a Uri matcher class within the ContentProvider Class ie;

public class MyContentProvider extends ContentProvider {
    
    private static final String TAG = AsanaProvider.class.getName(); // Helpful for debugging
    
    private static final String AUTHORITY = "com.example.myapp.MyContentProvider"; //Our Authority
    
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
      static {          
        sURIMatcher.addURI(AUTHORITY, "workspaces", 1); 
        sURIMatcher.addURI(AUTHORITY, "workspaces/projects/#",2);
      }
}

The UriMatcher is a handy class for constructing switch statements, like we will do in the following code for query().

Note the "YOUR API KEY KEY GOES HERE"; if you want to compile and test the code in the android app, you'll need to make an account at Asana and then copy and paste your api key in.

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        
        //Log.i(TAG,uri.toString());
        int match = sURIMatcher.match(uri);
        
        String[] columns = {"data"};
        MatrixCursor cursor = new MatrixCursor(columns);
        
        String url = "https://app.asana.com/api/1.0/workspaces";
        String encoding = Base64.encodeToString("YOUR_API_KEY_GOES_HERE:".getBytes(),2);
        
        
        StringBuilder baseurl= new StringBuilder("https://app.asana.com/api/1.0/");
        switch(match) {
        case    1:
            baseurl.append("workspaces");
            break;
        case    2:
            baseurl.append("workspaces/").append(uri.getLastPathSegment()).append("/projects");
            break;
        }
        
        AjaxCallback<String> cb = new AjaxCallback<String>();
        cb.url(baseurl.toString()).type(String.class).weakHandler(this,"stringCb");
        cb.header("Authorization", "Basic " + encoding);
        
        AQuery aq = new AQuery(getContext());
        
        aq.sync(cb);
        
        String jo = cb.getResult();
        AjaxStatus status = cb.getStatus();
        
        //Log.i(TAG, jo);
        //Log.i(TAG,status.toString());
        
        try {
            JSONObject workspaceWrapper = (JSONObject) new JSONTokener(jo).nextValue();
            JSONArray  workspaces       = workspaceWrapper.getJSONArray("data");
            
            for (int i = 0; i < workspaces.length(); i++) {
                String[] workspace = {workspaces.getJSONObject(i).toString()};
                cursor.addRow(workspace);
            }
        }
        catch (JSONException e) {
             Log.e(TAG, "Failed to parse JSON.", e);
        }
        
        return cursor;
    }

The switch statement will check what uri we have received, and then use it to build the corresponding uri request. The rest of the code is just making the http request and parsing the JSON response into a matrixcursor. Note that you must return a cursor; The easiest way to do this is by using MatrixCursor, which you construct by passing a String array which contains a list of column names to the constructor.

That's basically it.

To access the data in your activity, service etc can be done in a few different manners

  • Use a loader - which will involved creating a CursorLoader which can query the Uri directly, load everything in another thread, and keep everything all nice and organised (this is probably the best way)
  • Calling getContentResolver() in your activity and using the ContentResolver to directly query the ContentProvider - note that if your content provider is synchronous it will block the main thread, so make sure the call is asynchronous if you are going to do this. In my example, I've made a synchronous call.

Android Design Patterns recommend using loaders.

Now lets say we want to make some tests for content provider, to check that we've done everything properly. Androids test framework (based on junit) allows you to do this with the ProviderTestCase2 class, which will set up the MockContentResolver object that will allow us to test our Content Provider.

Using ProviderTestCase2 we request a MockContentResolver object and use this to make queries to the content provider. The setup() and tearDown() methods execute before each test and are used to perform initialisation. You can then define as many test cases as you like and use assertions to check that you are retrieving the correct data from the Content Provider.

The below is an example of how to set this class up;

package com.example.myapp.test;

import org.json.JSONException;
import org.json.JSONObject;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.test.ProviderTestCase2;
import android.test.mock.MockContentResolver;
import android.util.Log;

import com.example.myapp.MyContentProvider.*;

public class CPTester extends ProviderTestCase2<MyContentProvider> { // Extend your base class and replace the generic with your content provider
    
    private static final String TAG = CPTester.class.getName();
    
    private static MockContentResolver resolve; // in the test case scenario, we use the MockContentResolver to make queries
    
    public CPTester(Class<AsanaProvider> providerClass,String providerAuthority) {
        super(providerClass, providerAuthority);
        // TODO Auto-generated constructor stub
    }

    public CPTester() {
        //this.CPTester("com.example.myapp.MyContentProvider",AsanaProvider.class);
        super(MyContentProvider.class,"com.example.myapp.MyContentProvider");
    }
        

    @Override
    public void setUp() {
        try {
            Log.i(TAG, "Entered Setup");
            super.setUp();
            resolve = this.getMockContentResolver();
        }
        catch(Exception e) {
            
            
        }
    }
    
    @Override
    public void tearDown() {
        try{
            super.tearDown();
        }
        catch(Exception e) {
            
            
        }
    }
    
    public void testCase() {
        Log.i("TAG","Basic Insert Test");
    }
    
    public void testPreconditions() {
        // using this test to check data already inside my asana profile

        Log.i("TAG","Test Preconstructed Database");
        String[] projection = {"workspace_id","name"};
        String selection = null;
        String[] selectionArgs = null;
        String sortOrder = null;
        Cursor result = resolve.query(Uri.parse("content://com.example.myapp.MyContentProvider/workspace"), projection, selection, selectionArgs, sortOrder);
        
        assertEquals(result.getCount(), 3); //check number of returned rows
        assertEquals(result.getColumnCount(), 2); //check number of returned columns
        
        result.moveToNext();
        
        for(int i = 0; i < result.getCount(); i++) {
            String id = result.getString(0);
            String name = result.getString(1);
            Log.i("TAG",id + " : " + name);
            result.moveToNext();
        }
    }
}

Hopefully, this gives you a basic introduction to Content Providers on Android and how to test them.


About the Author

Matt Tyler is a software engineer & cloud tragic working @ Mechanical Rock. He helps teams get the most out of their cloud development experience.