Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle RESTful update of remote server with SyncAdapter

I've watched the Google I/O REST talk and read the slides: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html

I'm still a bit unclear on how to nicely handle, say, an update error thrown by the remote server. I have implemented my own ContentProvider and SyncAdapter. Consider this scenario:

Update a user's Contact Details via REST call:

  1. Request an update using a ContentResolver.
  2. My ContentProvider immediately updates the app's local Sqlite database and requests a Sync (as per recommendations in the Google I/O talk).
  3. My SyncAdapter.onPerformSync() is called and does a REST call to update the remote data.
  4. Remote server responds with "ERROR: Invalid Phone Number" (for instance).

My question is, what is the best way for the SyncAdapter to signal to my ContentProvider that this change needs to be backed out of the app's local database, and to also signal to my Activity that the update request failed (and pass the error messages returned from the Server)?

My activity needs to display a progress spinner while waiting for the result, and know whether the request succeeded or failed.


For updating the local app database with content from the Server, the SyncAdapter pattern makes complete sense to me, and I have that working fine. But for updates from the app to the server, I can't seem to find a nice way to handle the above scenario.


And another thing... ;)

Say I call ContentResolver.notifyChange(uri, null, true); from within my ContentProvider's update() method. true along with android:supportsUploading="true" will cause my SyncAdapter's onPerformSync() to be called. Great, but inside onPerformSync(), how do I tell what URI I should sync? I don't want to simply refresh my entire DB every time I get a Sync request. But you can't even pass a Bundle into the notifyChangeCall() to be passed on to onPerformSync().

All the examples I've seen of onPerformSync() have been so simple, and not using a custom ContentProvider, any real world examples out there? And the docs are a bit of a bird's nest. Virgil Dobjanschi, Sir, you've left me up the creek without a paddle.

like image 577
Jarrod Smith Avatar asked Nov 04 '11 00:11

Jarrod Smith


2 Answers

Short answer, if you're targeting ~API level 7, is "Don't". The situation may have improved in later APIs but as it was... I would strongly recommend avoiding SyncAdapter completely; it's documented very poorly and the "automatic" account/authentication management comes at a high price as the API for it is also convoluted and under-documented. This part of the API has not been thought through beyond the most trivial use cases.

So here's the pattern I ended up going with. Inside my activities I had a handler with a simple addition from a custom Handler superclass (could check for a m_bStopped bool):

private ResponseHandler mHandler = new ResponseHandler();

class ResponseHandler extends StopableHandler {

    @Override
    public void handleMessage(Message msg) {
        if (isStopped()) {
            return;
        }
        if (msg.what == WebAPIClient.GET_PLANS_RESPONSE) {
            ...
        } 
        ...
    }
}

The activity would invoke the REST requests as shown below. Notice, the handler is passed through to the WebClient class (a helper class for building/making the HTTP requests etc.). The WebClient uses this handler when it receives the HTTP response to message back to the activity and let it know the data has been received and, in my case, stored in an SQLite database (which I would recommend). In most Activities, I would call mHandler.stopHandler(); in onPause() and mHandler.startHandler(); in onResume() to avoid the HTTP response being signalled back to an inactive Activity etc. This turned out to be quite a robust approach.

final Bundle bundle = new Bundle();
bundle.putBoolean(WebAPIRequestHelper.REQUEST_CREATESIMKITORDER, true);
bundle.putString(WebAPIRequestHelper.REQUEST_PARAM_KIT_TYPE, sCVN);       
final Runnable runnable = new Runnable() { public void run() {
    VendApplication.getWebClient().processRequest(null, bundle, null, null, null,
                    mHandler, NewAccountActivity.this);
    }};
mRequestThread = Utils.performOnBackgroundThread(runnable);

Handler.handleMessage() is invoked on the main thread. So you can stop your progress dialogs here and do other Activity stuff safely.

I declared a ContentProvider:

<provider android:name="au.com.myproj.android.app.webapi.WebAPIProvider"
          android:authorities="au.com.myproj.android.app.provider.webapiprovider"
          android:syncable="true" />

And implemented it to create and manage access to the SQLite db:

public class WebAPIProvider extends ContentProvider

So you can then get cursors over the database in your Activities like this:

mCursor = this.getContentResolver().query (
          WebAPIProvider.PRODUCTS_URI, null, 
          Utils.getProductsWhereClause(this), null, 
          Utils.getProductsOrderClause(this));
startManagingCursor(mCursor);

I found the org.apache.commons.lang3.text.StrSubstitutor class to be immensely helpful in constructing the clumsy XML requests required by the REST API I had to integrate with e.g. in WebAPIRequestHelper I had helper methods like:

public static String makeAuthenticateQueryString(Bundle params)
{
    Map<String, String> valuesMap = new HashMap<String, String>();
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTNUMBER);
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTPASSWORD);

    valuesMap.put(REQUEST_PARAM_APIUSERNAME, API_USERNAME);
    valuesMap.put(REQUEST_PARAM_ACCOUNTNUMBER, params.getString(REQUEST_PARAM_ACCOUNTNUMBER));
    valuesMap.put(REQUEST_PARAM_ACCOUNTPASSWORD, params.getString(REQUEST_PARAM_ACCOUNTPASSWORD));

    String xmlTemplate = VendApplication.getContext().getString(R.string.XMLREQUEST_AUTHENTICATE_ACCOUNT);
    StrSubstitutor sub = new StrSubstitutor(valuesMap);
    return sub.replace(xmlTemplate);
}

Which I would append to the appropriate endpoint URL.

Here's some more details on how the WebClient class does the HTTP requests. This is the processRequest() method called earlier in the Runnable. Notice the handler parameter which is used to message the results back to the ResponseHandler I described above. The syncResult is in out parameter used by the SyncAdapter to do exponential backoff etc. I use it in the executeRequest(), incrementing it's various error counts etc. Again, very poorly documented and a PITA to get working. parseXML() leverages the superb Simple XML lib.

public synchronized void processRequest(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult, Handler handler, Context context)
{
    // Helper to construct the query string from the query params passed in the extras Bundle.
    HttpUriRequest request = createHTTPRequest(extras);
    // Helper to perform the HTTP request using org.apache.http.impl.client.DefaultHttpClient.
    InputStream instream = executeRequest(request, syncResult);

    /*
     * Process the result.
     */
    if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETBALANCE))
    {
        GetServiceBalanceResponse xmlDoc = parseXML(GetServiceBalanceResponse.class, instream, syncResult);
        Assert.assertNotNull(handler);
        Message m = handler.obtainMessage(WebAPIClient.GET_BALANCE_RESPONSE, xmlDoc);
        m.sendToTarget();
    }
    else if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETACCOUNTINFO))
    {
      ...
    }
    ...

}

You should put some timeouts on the HTTP requests so the app doesn't wait forever if the mobile data drops out, or it switches from Wifi to 3G. This will cause an exception to be thrown if the timeout occurs.

    // Set the timeout in milliseconds until a connection is established.
    int timeoutConnection = 30000;
    HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
    // Set the default socket timeout (SO_TIMEOUT) in milliseconds which is the timeout for waiting for data.
    int timeoutSocket = 30000;
    HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
    HttpClient client = new DefaultHttpClient(httpParameters);          

So overall, the SyncAdapter and Accounts stuff was a total pain and cost me a lot of time for no gain. The ContentProvider was fairly useful, mainly for the cursor and transaction support. The SQLite database was really good. And the Handler class is awesome. I would use the AsyncTask class now instead of creating your own Threads like I did above to spawn the HTTP requests.

I hope this rambling explanation helps someone a bit.

like image 174
Jarrod Smith Avatar answered Oct 23 '22 19:10

Jarrod Smith


What about the Observer design pattern? Can your activity be an observer of the SyncAdapter or database? That way when an update fails, the adapter will notify its observers and can then act on the data that changed. There are a bunch of Observable classes in the SDK, see which one works best in your situation. http://developer.android.com/search.html#q=Observer&t=0

like image 20
smith324 Avatar answered Oct 23 '22 18:10

smith324