package org.dodgybits.shuffle.android.synchronisation.tracks;
import static org.dodgybits.shuffle.android.core.util.Constants.cFlurryTracksSyncError;
import java.io.StringReader;
import java.text.ParseException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.dodgybits.shuffle.android.core.model.EntityBuilder;
import org.dodgybits.shuffle.android.core.model.Id;
import org.dodgybits.shuffle.android.core.model.persistence.EntityPersister;
import org.dodgybits.shuffle.android.persistence.provider.ContextProvider;
import org.dodgybits.shuffle.android.persistence.provider.ProjectProvider;
import org.dodgybits.shuffle.android.preference.view.Progress;
import org.dodgybits.shuffle.android.synchronisation.tracks.model.TracksEntity;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;
import com.flurry.android.FlurryAgent;
/**
* Base class for handling synchronization, template method object.
*
* @author Morten Nielsen
*/
public abstract class Synchronizer<Entity extends TracksEntity> {
private static final String cTag = "Synchronizer";
protected EntityPersister<Entity> mPersister;
protected WebClient mWebClient;
protected android.content.Context mContext;
protected final TracksSynchronizer mTracksSynchronizer;
private int mBasePercent;
public Synchronizer(
EntityPersister<Entity> persister,
TracksSynchronizer tracksSynchronizer,
WebClient client,
android.content.Context context,
int basePercent) {
mPersister = persister;
mTracksSynchronizer = tracksSynchronizer;
mWebClient = client;
mContext = context;
mBasePercent = basePercent;
}
public void synchronize() throws WebClient.ApiException {
mTracksSynchronizer.reportProgress(Progress.createProgress(mBasePercent,
readingLocalText()));
Map<Id, Entity> localEntities = getShuffleEntities();
verifyLocalEntities(localEntities);
mTracksSynchronizer.reportProgress(Progress.createProgress(mBasePercent,
readingRemoteText()));
TracksEntities tracksEntities = getTrackEntities();
int startCounter = localEntities.size() + 1;
int count = 0;
for (Entity localEntity : localEntities.values()) {
count++;
int percent = mBasePercent
+ Math.round(((count * 100) / startCounter) * 0.33f);
mTracksSynchronizer.reportProgress(Progress.createProgress(percent,
processingText()));
synchronizeSingle(tracksEntities, localEntity);
}
for (Entity remoteEntity : tracksEntities.getEntities().values()) {
insertEntity(remoteEntity);
}
mTracksSynchronizer.reportProgress(Progress.createProgress(
mBasePercent + 33, stageFinishedText()));
}
protected Id findProjectIdByTracksId(Id tracksId) {
return findEntityLocalIdByTracksId(tracksId, ProjectProvider.Projects.CONTENT_URI);
}
protected Id findContextIdByTracksId(Id tracksId) {
return findEntityLocalIdByTracksId(tracksId, ContextProvider.Contexts.CONTENT_URI);
}
protected Id findTracksIdByProjectId(Id projectId) {
return findEntityTracksIdByLocalId(projectId, ProjectProvider.Projects.CONTENT_URI);
}
protected Id findTracksIdByContextId(Id contextId) {
return findEntityTracksIdByLocalId(contextId, ContextProvider.Contexts.CONTENT_URI);
}
protected abstract void verifyLocalEntities(Map<Id, Entity> localEntities);
protected abstract String readingRemoteText();
protected abstract String processingText();
protected abstract String readingLocalText();
protected abstract String stageFinishedText();
protected abstract String endIndexTag();
protected abstract String entityIndexUrl();
protected abstract Entity createMergedLocalEntity(Entity localEntity,
Entity newEntity);
protected abstract String createEntityUrl(Entity localEntity);
protected abstract String createDocumentForEntity(Entity localEntity);
protected abstract Entity parseSingleEntity(XmlPullParser parser)
throws ParseException;
protected abstract EntityBuilder<Entity> createBuilder();
private TracksEntities getTrackEntities() throws WebClient.ApiException {
Map<Id, Entity> entities = new HashMap<Id, Entity>();
boolean errorFree = true;
String tracksEntityXml;
try {
tracksEntityXml = mWebClient.getUrlContent(entityIndexUrl());
} catch (WebClient.ApiException e) {
Log.w(cTag, e);
throw e;
}
XmlPullParser parser = Xml.newPullParser();
try {
parser.setInput(new StringReader(tracksEntityXml));
int eventType = parser.getEventType();
boolean done = false;
while (eventType != XmlPullParser.END_DOCUMENT && !done) {
Entity entity = null;
try {
entity = parseSingleEntity(parser);
} catch (Exception e) {
logTracksError(e);
errorFree = false;
}
if (entity != null && entity.isInitialized()) {
entities.put(entity.getTracksId(), entity);
}
eventType = parser.getEventType();
String name = parser.getName();
if (eventType == XmlPullParser.END_TAG &&
name.equalsIgnoreCase(endIndexTag())) {
done = true;
}
}
} catch (XmlPullParserException e) {
logTracksError(e);
errorFree = false;
}
return new TracksEntities(entities, errorFree);
}
private void logTracksError(Exception e) {
Log.e(cTag, "Failed to parse " + endIndexTag() + " " + e.getMessage());
FlurryAgent.onError(cFlurryTracksSyncError, e.getMessage(), getClass().getName());
}
private Id findEntityLocalIdByTracksId(Id tracksId, Uri contentUri) {
Id id = Id.NONE;
Cursor cursor = mContext.getContentResolver().query(
contentUri,
new String[] { BaseColumns._ID},
"tracks_id = ?",
new String[] {tracksId.toString()},
null);
if (cursor.moveToFirst()) {
id = Id.create(cursor.getLong(0));
}
cursor.close();
return id;
}
private Id findEntityTracksIdByLocalId(Id localId, Uri contentUri) {
Id id = Id.NONE;
Cursor cursor = mContext.getContentResolver().query(
contentUri,
new String[] { "tracks_id" },
BaseColumns._ID + " = ?",
new String[] {localId.toString()},
null);
if (cursor.moveToFirst()) {
id = Id.create(cursor.getLong(0));
}
cursor.close();
return id;
}
private void insertEntity(Entity entity) {
mPersister.insert(entity);
}
private void updateEntity(Entity entity) {
mPersister.update(entity);
}
private boolean deleteEntity(Entity entity)
{
return mPersister.delete(entity.getLocalId());
}
private Entity findEntityByLocalName(Collection<Entity> remoteEntities,
Entity localEntity) {
Entity foundEntity = null;
for (Entity entity : remoteEntities)
if (entity.getLocalName().equals(localEntity.getLocalName())) {
foundEntity = entity;
}
return foundEntity;
}
private void synchronizeSingle(TracksEntities tracksEntities,
Entity localEntity) {
final Map<Id, Entity> remoteEntities = tracksEntities.getEntities();
if (!localEntity.getTracksId().isInitialised()) {
Entity newEntity = findEntityByLocalName(remoteEntities.values(),
localEntity);
if (newEntity != null) {
remoteEntities.remove(newEntity.getTracksId());
} else {
newEntity = createEntityInTracks(localEntity);
}
if (newEntity != null) {
updateEntity(createMergedLocalEntity(localEntity, newEntity));
}
return;
}
Entity remoteEntity = remoteEntities.get(localEntity.getTracksId());
if (remoteEntity != null) {
handleRemoteEntity(localEntity, remoteEntity);
remoteEntities.remove(remoteEntity.getTracksId());
} else if (tracksEntities.isErrorFree()){
// only delete entities if we didn't encounter errors parsing
deleteEntity(localEntity);
}
}
private void handleRemoteEntity(Entity localEntity, Entity remoteEntity) {
final long remoteModified = remoteEntity.getModifiedDate();
final long localModified = localEntity.getModifiedDate();
if (remoteModified == localModified)
return;
if (remoteModified > localModified) {
updateEntity(createMergedLocalEntity(localEntity, remoteEntity));
} else {
updateTracks(localEntity);
}
}
private void updateTracks(Entity localEntity) {
String document = createDocumentForEntity(localEntity);
try {
mWebClient.putContentToUrl(createEntityUrl(localEntity), document);
} catch (WebClient.ApiException e) {
Log.w(cTag, "Failed to update entity in tracks " + localEntity + ":" + e.getMessage(), e);
}
}
private Entity createEntityInTracks(Entity entity) {
String document = createDocumentForEntity(entity);
try {
String location = mWebClient.postContentToUrl(entityIndexUrl(),
document);
if (!TextUtils.isEmpty(location.trim())) {
Id id = parseIdFromLocation(location);
EntityBuilder<Entity> builder = createBuilder();
builder.mergeFrom(entity);
builder.setTracksId(id);
entity = builder.build();
}
} catch (WebClient.ApiException e) {
Log.w(cTag, "Failed to create entity in tracks " + entity + ":" + e.getMessage(), e);
}
return entity;
}
private Id parseIdFromLocation(String location) {
String[] parts = location.split("/");
String document = parts[parts.length - 1];
long id = Long.parseLong( document );
return Id.create(id);
}
private Map<Id, Entity> getShuffleEntities() {
Map<Id, Entity> list = new HashMap<Id, Entity>();
Cursor cursor = mContext.getContentResolver().query(
mPersister.getContentUri(),
mPersister.getFullProjection(),
null, null, null);
while (cursor.moveToNext()) {
Entity entity = mPersister.read(cursor);
list.put(entity.getLocalId(), entity);
}
cursor.close();
return list;
}
private class TracksEntities {
private Map<Id, Entity> mEntities;
private boolean mErrorFree;
public TracksEntities(Map<Id, Entity> entities, boolean errorFree) {
mEntities = entities;
mErrorFree = errorFree;
}
public Map<Id, Entity> getEntities() {
return mEntities;
}
public boolean isErrorFree() {
return mErrorFree;
}
}
}