/* * Copyright 2015 The Project Buendia Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at: http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distrib- * uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * OR CONDITIONS OF ANY KIND, either express or implied. See the License for * specific language governing permissions and limitations under the License. */ package org.projectbuendia.client.sync.controllers; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.SyncResult; import android.net.Uri; import android.support.annotation.Nullable; import com.android.volley.DefaultRetryPolicy; import com.android.volley.Response; import com.android.volley.toolbox.RequestFuture; import org.projectbuendia.client.App; import org.projectbuendia.client.json.IncrementalSyncResponse; import org.projectbuendia.client.json.Serializers; import org.projectbuendia.client.net.Common; import org.projectbuendia.client.net.GsonRequest; import org.projectbuendia.client.net.OpenMrsConnectionDetails; import org.projectbuendia.client.providers.Contracts; import org.projectbuendia.client.sync.SyncAdapter; import org.projectbuendia.client.utils.Logger; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import static org.projectbuendia.client.net.OpenMrsServer.wrapErrorListener; /** * Implements the basic logic for an incremental sync phase. * <p> * To implement an incremental sync phase, create a subclass, supply the appropriate arguments to * {@link IncrementalSyncPhaseRunnable}'s constructor from the subclasses' public, no-arg * constructor, and then implement the {@link #getUpdateOps(Object[], SyncResult)} method. * <p> * Note: you may also wish to undertake an action at the start and end of the sync phase - hooks are * provided for this. See {@link #beforeSyncStarted(ContentResolver, SyncResult, * ContentProviderClient)} and {@link #afterSyncFinished(ContentResolver, SyncResult, * ContentProviderClient)}. */ public abstract class IncrementalSyncPhaseRunnable<T> implements SyncPhaseRunnable { private static final Logger LOG = Logger.create(); private final String resourceType; private final Contracts.Table dbTable; private final Class<T> clazz; /** * Instantiate a new IncrementalSyncPhaseRunnable. This is designed to be called from a no-arg * constructor of subclasses. * * @param resourceType the type of resource to be fetched. This string is appended to * {@link OpenMrsConnectionDetails#getBuendiaApiUrl()} to determine the HTTP * endpoint to request from. * @param dbTable the database table to store fetched data in. Also used as a key against * which sync tokens from the server will be stored. * @param clazz the {@link Class} object corresponding to the generic type {@code T}. */ protected IncrementalSyncPhaseRunnable( String resourceType, Contracts.Table dbTable, Class<T> clazz) { this.resourceType = resourceType; this.dbTable = dbTable; this.clazz = clazz; } @Override public final void sync(ContentResolver contentResolver, SyncResult syncResult, ContentProviderClient providerClient) throws Throwable { beforeSyncStarted(contentResolver, syncResult, providerClient); String syncToken = SyncAdapter.getLastSyncToken(providerClient, dbTable); LOG.i("Using sync token `%s`", syncToken); IncrementalSyncResponse<T> response; do { RequestFuture<IncrementalSyncResponse<T>> future = RequestFuture.newFuture(); createRequest(syncToken, future, future); response = future.get(); ArrayList<ContentProviderOperation> ops = getUpdateOps(response.results, syncResult); providerClient.applyBatch(ops); LOG.i("Updated page of %s (%d db ops)", resourceType, ops.size()); // Update sync token syncToken = response.syncToken; } while (response.more); LOG.i("Saving new sync token `%s`", syncToken); SyncAdapter.storeSyncToken(providerClient, dbTable, response.syncToken); afterSyncFinished(contentResolver, syncResult, providerClient); } // Mandatory callback /** * Produces a list of the operations needed to bring the local database in sync with the server. */ protected abstract ArrayList<ContentProviderOperation> getUpdateOps( T[] list, SyncResult syncResult); // Optional callbacks /** Called before any records have been synced from the server. */ protected void beforeSyncStarted( ContentResolver contentResolver, SyncResult syncResult, ContentProviderClient providerClient) throws Throwable {} /** * Called after all records have been synced from the server, even if the number of synced * records was zero. */ protected void afterSyncFinished( ContentResolver contentResolver, SyncResult syncResult, ContentProviderClient providerClient) throws Throwable {} private void createRequest( @Nullable String lastSyncToken, Response.Listener<IncrementalSyncResponse<T>> successListener, final Response.ErrorListener errorListener) { OpenMrsConnectionDetails connectionDetails = App.getConnectionDetails(); Uri.Builder url = Uri.parse(connectionDetails.getBuendiaApiUrl()).buildUpon(); url.appendPath(resourceType); if (lastSyncToken != null) { url.appendQueryParameter("since", lastSyncToken); } GsonRequest<IncrementalSyncResponse<T>> request = new GsonRequest<>( url.build().toString(), new IncrementalSyncResponseType(clazz), connectionDetails.addAuthHeader(new HashMap<String, String>()), successListener, wrapErrorListener(errorListener)); Serializers.registerTo(request.getGson()); request.setRetryPolicy( new DefaultRetryPolicy(Common.REQUEST_TIMEOUT_MS_MEDIUM, 1, 1f)); connectionDetails.getVolley().addToRequestQueue(request); } private static class IncrementalSyncResponseType implements ParameterizedType { private final Type[] typeArgs; public IncrementalSyncResponseType(Type innerType) { typeArgs = new Type[] { innerType }; } @Override public Type[] getActualTypeArguments() { return typeArgs; } @Override public Type getOwnerType() { return null; } @Override public Type getRawType() { return IncrementalSyncResponse.class; } } }