/* * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package at.bitfire.davdroid.syncadapter; import android.content.SyncResult; import android.util.Log; import java.io.IOException; import java.net.URISyntaxException; import java.util.HashSet; import java.util.Set; import at.bitfire.davdroid.ArrayUtils; import at.bitfire.davdroid.resource.LocalCollection; import at.bitfire.davdroid.resource.LocalStorageException; import at.bitfire.davdroid.resource.RecordNotFoundException; import at.bitfire.davdroid.resource.Resource; import at.bitfire.davdroid.resource.WebDavCollection; import at.bitfire.davdroid.webdav.ConflictException; import at.bitfire.davdroid.webdav.DavException; import at.bitfire.davdroid.webdav.ForbiddenException; import at.bitfire.davdroid.webdav.HttpException; import at.bitfire.davdroid.webdav.NotFoundException; import at.bitfire.davdroid.webdav.PreconditionFailedException; import at.bitfire.davdroid.webdav.WebDavResource; public class SyncManager { private static final String TAG = "davdroid.SyncManager"; private static final int MAX_MULTIGET_RESOURCES = 35; final protected LocalCollection local; final protected WebDavCollection remote; public SyncManager(LocalCollection local, WebDavCollection remote) { this.local = local; this.remote = remote; } public void synchronize(boolean manualSync, SyncResult syncResult) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { // PHASE 1: fetch collection properties remote.getProperties(); final WebDavResource.Properties collectionProperties = remote.getCollection().getProperties(); local.updateMetaData(collectionProperties); // PHASE 2: push local changes to server int deletedRemotely = pushDeleted(), addedRemotely = pushNew(), updatedRemotely = pushDirty(); // PHASE 3A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed) boolean syncMembers = (deletedRemotely + addedRemotely + updatedRemotely) > 0; if (manualSync) { Log.i(TAG, "Full synchronization forced"); syncMembers = true; } if (!syncMembers) { final String currentCTag = collectionProperties.getCTag(), lastCTag = local.getCTag(); Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag); if (currentCTag == null || !currentCTag.equals(lastCTag)) syncMembers = true; } if (!syncMembers) { Log.i(TAG, "No local changes and CTags match, no need to sync"); return; } // PHASE 3B: detect details of remote changes Log.i(TAG, "Fetching remote resource list"); Set remotelyAdded = new HashSet<>(), remotelyUpdated = new HashSet<>(); Resource[] remoteResources = remote.getMemberETags(); for (Resource remoteResource : remoteResources) { try { Resource localResource = local.findByRemoteName(remoteResource.getName(), false); if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag())) remotelyUpdated.add(remoteResource); } catch(RecordNotFoundException e) { remotelyAdded.add(remoteResource); } } // PHASE 4: pull remote changes from server syncResult.stats.numInserts = pullNew(remotelyAdded.toArray(new Resource[remotelyAdded.size()])); syncResult.stats.numUpdates = pullChanged(remotelyUpdated.toArray(new Resource[remotelyUpdated.size()])); Log.i(TAG, "Removing entries that are not present remotely anymore (retaining " + remoteResources.length + " entries)"); syncResult.stats.numDeletes = local.deleteAllExceptRemoteNames(remoteResources); syncResult.stats.numEntries = syncResult.stats.numInserts + syncResult.stats.numUpdates + syncResult.stats.numDeletes; // update collection CTag Log.i(TAG, "Sync complete, fetching new CTag"); local.setCTag(collectionProperties.getCTag()); } private int pushDeleted() throws URISyntaxException, LocalStorageException, IOException, HttpException { int count = 0; long[] deletedIDs = local.findDeleted(); try { Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)"); for (long id : deletedIDs) try { Resource res = local.findById(id, false); if (res.getName() != null) // is this resource even present remotely? try { remote.delete(res); } catch(NotFoundException e) { Log.i(TAG, "Locally-deleted resource has already been removed from server"); } catch(PreconditionFailedException|ConflictException e) { Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile"); } local.delete(res); count++; } catch (RecordNotFoundException e) { Log.wtf(TAG, "Couldn't read locally-deleted record", e); } } finally { local.commit(); } return count; } private int pushNew() throws URISyntaxException, LocalStorageException, IOException, HttpException { int count = 0; long[] newIDs = local.findNew(); Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)"); try { for (long id : newIDs) try { Resource res = local.findById(id, true); String eTag = remote.add(res); if (eTag != null) local.updateETag(res, eTag); local.clearDirty(res); count++; } catch (ConflictException|PreconditionFailedException e) { Log.i(TAG, "Didn't overwrite existing resource with other content"); } catch (RecordNotFoundException e) { Log.wtf(TAG, "Couldn't read new record", e); } } finally { local.commit(); } return count; } private int pushDirty() throws URISyntaxException, LocalStorageException, IOException, HttpException { int count = 0; long[] dirtyIDs = local.findUpdated(); Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)"); try { for (long id : dirtyIDs) { try { Resource res = local.findById(id, true); String eTag = remote.update(res); if (eTag != null) local.updateETag(res, eTag); local.clearDirty(res); count++; } catch (ForbiddenException e) { Log.w(TAG, "Server has rejected local changes, server wins", e); } catch (ConflictException|PreconditionFailedException e) { Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile", e); } catch (RecordNotFoundException e) { Log.e(TAG, "Couldn't read dirty record", e); } } } finally { local.commit(); } return count; } private int pullNew(Resource[] resourcesToAdd) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { int count = 0; Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)"); for (Resource[] resources : ArrayUtils.partition(resourcesToAdd, MAX_MULTIGET_RESOURCES)) for (Resource res : remote.multiGet(resources)) { Log.d(TAG, "Adding " + res.getName()); local.add(res); count++; } return count; } private int pullChanged(Resource[] resourcesToUpdate) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { int count = 0; Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)"); for (Resource[] resources : ArrayUtils.partition(resourcesToUpdate, MAX_MULTIGET_RESOURCES)) for (Resource res : remote.multiGet(resources)) { Log.i(TAG, "Updating " + res.getName()); local.updateByRemoteName(res); count++; } return count; } }