package org.projectbuendia.client.sync.controllers;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import com.android.volley.toolbox.RequestFuture;
import org.projectbuendia.client.App;
import org.projectbuendia.client.json.JsonLocation;
import org.projectbuendia.client.providers.Contracts.LocationNames;
import org.projectbuendia.client.providers.Contracts.Locations;
import org.projectbuendia.client.utils.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* Handles syncing locations. All locations are always fetched, which is ok because the full set of
* locations is fairly smaller.
*/
public class LocationsSyncPhaseRunnable implements SyncPhaseRunnable {
private static final Logger LOG = Logger.create();
@Override
public void sync(ContentResolver contentResolver, SyncResult syncResult,
ContentProviderClient providerClient)
throws Throwable {
ArrayList<ContentProviderOperation> ops = getLocationUpdateOps(syncResult);
providerClient.applyBatch(ops);
contentResolver.notifyChange(Locations.CONTENT_URI, null, false);
contentResolver.notifyChange(LocationNames.CONTENT_URI, null, false);
}
/**
* Requests locations from the server and transforms the response into an {@link ArrayList} of
* {@link ContentProviderOperation}s for updating the database.
*/
private static ArrayList<ContentProviderOperation> getLocationUpdateOps(SyncResult syncResult)
throws ExecutionException, InterruptedException {
final ContentResolver contentResolver = App.getInstance().getContentResolver();
final String[] projection = new String[] {
Locations.UUID,
Locations.PARENT_UUID
};
final String[] namesProjection = new String[] {
LocationNames.LOCATION_UUID,
LocationNames.LOCALE,
LocationNames.NAME
};
LOG.d("Before network call");
RequestFuture<List<JsonLocation>> future = RequestFuture.newFuture();
App.getServer().listLocations(future, future);
// No need for callbacks as the {@AbstractThreadedSyncAdapter} code is executed in a
// background thread
List<JsonLocation> locations = future.get();
LOG.d("After network call");
ArrayList<ContentProviderOperation> batch = new ArrayList<>();
Map<String, JsonLocation> locationsByUuid = new HashMap<>();
for (JsonLocation location : locations) {
locationsByUuid.put(location.uuid, location);
}
// Get list of all items
Uri uri = Locations.CONTENT_URI; // Location tree
Uri namesUri = LocationNames.CONTENT_URI; // Location names
Cursor c = contentResolver.query(uri, projection, null, null, null);
assert c != null;
Cursor namesCur = contentResolver.query(namesUri, namesProjection, null, null, null);
assert namesCur != null;
LOG.i("Examining locations: %d local, %d from server", c.getCount(), locations.size());
String uuid;
String parentUuid;
// Build map of location names from the database.
Map<String, Map<String, String>> dbLocationNames = new HashMap<>();
while (namesCur.moveToNext()) {
String locationUuid = namesCur.getString(
namesCur.getColumnIndex(LocationNames.LOCATION_UUID));
String locale = namesCur.getString(
namesCur.getColumnIndex(LocationNames.LOCALE));
String name = namesCur.getString(
namesCur.getColumnIndex(LocationNames.NAME));
if (locationUuid == null || locale == null || name == null) continue;
if (!dbLocationNames.containsKey(locationUuid)) {
dbLocationNames.put(locationUuid, new HashMap<String, String>());
}
dbLocationNames.get(locationUuid).put(locale, name);
}
namesCur.close();
// Iterate through the list of locations
while (c.moveToNext()) {
syncResult.stats.numEntries++;
uuid = c.getString(c.getColumnIndex(Locations.UUID));
parentUuid = c.getString(c.getColumnIndex(Locations.PARENT_UUID));
JsonLocation location = locationsByUuid.get(uuid);
if (location != null) {
// Entry exists. Remove from entry map to prevent insert later.
locationsByUuid.remove(uuid);
// Grab the names stored in the database for this location.
Map<String, String> locationNames = dbLocationNames.get(uuid);
// Check to see if the entry needs to be updated
Uri existingUri = uri.buildUpon().appendPath(String.valueOf(uuid)).build();
if (location.parent_uuid != null && !location.parent_uuid.equals(parentUuid)) {
// Update existing record
LOG.i(" - will update location " + uuid);
batch.add(ContentProviderOperation.newUpdate(existingUri)
.withValue(Locations.UUID, uuid)
.withValue(Locations.PARENT_UUID, parentUuid)
.build());
syncResult.stats.numUpdates++;
}
if (location.names != null
&& (locationNames == null || !location.names.equals(locationNames))) {
Uri existingNamesUri = namesUri.buildUpon().appendPath(
String.valueOf(uuid)).build();
// Update location names by deleting any existing location names and
// repopulating.
batch.add(ContentProviderOperation.newDelete(existingNamesUri).build());
syncResult.stats.numDeletes++;
for (String locale : location.names.keySet()) {
batch.add(ContentProviderOperation.newInsert(existingNamesUri)
.withValue(LocationNames.LOCATION_UUID, uuid)
.withValue(LocationNames.LOCALE, locale)
.withValue(LocationNames.NAME, location.names.get(locale))
.build());
syncResult.stats.numInserts++;
}
}
} else {
// Entry doesn't exist. Remove it from the database.
LOG.i(" - will delete location " + uuid);
Uri deleteUri = uri.buildUpon().appendPath(uuid).build();
batch.add(ContentProviderOperation.newDelete(deleteUri).build());
syncResult.stats.numDeletes++;
Uri namesDeleteUri = namesUri.buildUpon().appendPath(uuid).build();
batch.add(ContentProviderOperation.newDelete(namesDeleteUri).build());
syncResult.stats.numDeletes++;
}
}
c.close();
for (JsonLocation location : locationsByUuid.values()) {
LOG.i(" - will insert location " + location.uuid);
batch.add(ContentProviderOperation.newInsert(Locations.CONTENT_URI)
.withValue(Locations.UUID, location.uuid)
.withValue(Locations.PARENT_UUID, location.parent_uuid)
.build());
syncResult.stats.numInserts++;
if (location.names != null) {
for (String locale : location.names.keySet()) {
Uri existingNamesUri = namesUri.buildUpon().appendPath(
String.valueOf(location.uuid)).build();
batch.add(ContentProviderOperation.newInsert(existingNamesUri)
.withValue(LocationNames.LOCATION_UUID, location.uuid)
.withValue(LocationNames.LOCALE, locale)
.withValue(LocationNames.NAME, location.names.get(locale))
.build());
syncResult.stats.numInserts++;
}
}
}
return batch;
}
}