@ -1,16 +1,15 @@
/ *
/ *
* Copyright © 2013 – 2015 Ricki Hirner ( bitfire web engineering ) .
* Copyright © 2013 – 2015 Ricki Hirner ( bitfire web engineering ) .
* All rights reserved . This program and the accompanying materials
* All rights reserved . This program and the accompanying materials
* are made available under the terms of the GNU Public License v3 .0
* are made available under the terms of the GNU Public License v3 .0
* which accompanies this distribution , and is available at
* which accompanies this distribution , and is available at
* http : //www.gnu.org/licenses/gpl.html
* http : //www.gnu.org/licenses/gpl.html
* /
* /
package at.bitfire.davdroid.syncadapter ;
package at.bitfire.davdroid.syncadapter ;
import android.accounts.Account ;
import android.accounts.Account ;
import android.annotation.TargetApi ;
import android.annotation.TargetApi ;
import android.app.PendingIntent ;
import android.app.PendingIntent ;
import android.content.ContentResolver ;
import android.content.Context ;
import android.content.Context ;
import android.content.Intent ;
import android.content.Intent ;
import android.content.SyncResult ;
import android.content.SyncResult ;
@ -18,55 +17,49 @@ import android.net.Uri;
import android.os.Bundle ;
import android.os.Bundle ;
import android.support.v4.app.NotificationManagerCompat ;
import android.support.v4.app.NotificationManagerCompat ;
import android.support.v7.app.NotificationCompat ;
import android.support.v7.app.NotificationCompat ;
import android.text.TextUtils ;
import java.io.FileNotFoundException ;
import java.io.IOException ;
import java.io.IOException ;
import java.util.ArrayList ;
import java.util.Date ;
import java.util.Date ;
import java.util.HashMap ;
import java.util.HashMap ;
import java.util.HashSet ;
import java.util.LinkedList ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.Set ;
import java.util.UUID ;
import java.util.UUID ;
import java.util.logging.Level ;
import java.util.logging.Level ;
import at.bitfire.dav4android.DavResource ;
import at.bitfire.dav4android.exception.ConflictException ;
import at.bitfire.dav4android.exception.DavException ;
import at.bitfire.dav4android.exception.HttpException ;
import at.bitfire.dav4android.exception.PreconditionFailedException ;
import at.bitfire.dav4android.exception.ServiceUnavailableException ;
import at.bitfire.dav4android.exception.UnauthorizedException ;
import at.bitfire.dav4android.property.GetCTag ;
import at.bitfire.dav4android.property.GetETag ;
import at.bitfire.davdroid.AccountSettings ;
import at.bitfire.davdroid.AccountSettings ;
import at.bitfire.davdroid.App ;
import at.bitfire.davdroid.App ;
import at.bitfire.davdroid.GsonHelper ;
import at.bitfire.davdroid.HttpClient ;
import at.bitfire.davdroid.HttpClient ;
import at.bitfire.davdroid.InvalidAccountException ;
import at.bitfire.davdroid.InvalidAccountException ;
import at.bitfire.davdroid.R ;
import at.bitfire.davdroid.R ;
import at.bitfire.davdroid.journalmanager.Exceptions ;
import at.bitfire.davdroid.journalmanager.JournalEntryManager ;
import at.bitfire.davdroid.resource.LocalCollection ;
import at.bitfire.davdroid.resource.LocalCollection ;
import at.bitfire.davdroid.resource.LocalResource ;
import at.bitfire.davdroid.resource.LocalResource ;
import at.bitfire.davdroid.ui.AccountSettingsActivity ;
import at.bitfire.davdroid.ui.AccountSettingsActivity ;
import at.bitfire.davdroid.ui.DebugInfoActivity ;
import at.bitfire.davdroid.ui.DebugInfoActivity ;
import at.bitfire.ical4android.CalendarStorageException ;
import at.bitfire.ical4android.CalendarStorageException ;
import at.bitfire.ical4android.InvalidCalendarException ;
import at.bitfire.vcard4android.ContactsStorageException ;
import at.bitfire.vcard4android.ContactsStorageException ;
import okhttp3.HttpUrl ;
import lombok.Getter ;
import okhttp3.OkHttpClient ;
import okhttp3.OkHttpClient ;
import okhttp3.RequestBody ;
abstract public class SyncManager {
abstract public class SyncManager {
protected final int SYNC_PHASE_PREPARE = 0 ,
protected final String SYNC_PHASE_PREPARE = "sync_phase_prepare" ,
SYNC_PHASE_QUERY_CAPABILITIES = 1 ,
SYNC_PHASE_QUERY_CAPABILITIES = "sync_phase_query_capabilities" ,
SYNC_PHASE_PROCESS_LOCALLY_DELETED = 2 ,
SYNC_PHASE_PREPARE_LOCAL = "sync_phase_prepare_local" ,
SYNC_PHASE_PREPARE_DIRTY = 3 ,
SYNC_PHASE_CREATE_LOCAL_ENTRIES = "sync_phase_create_local_entries" ,
SYNC_PHASE_UPLOAD_DIRTY = 4 ,
SYNC_PHASE_FETCH_ENTRIES = "sync_phase_fetch_entries" ,
SYNC_PHASE_CHECK_SYNC_STATE = 5 ,
SYNC_PHASE_APPLY_REMOTE_ENTRIES = "sync_phase_apply_remote_entries" ,
SYNC_PHASE_LIST_LOCAL = 6 ,
SYNC_PHASE_APPLY_LOCAL_ENTRIES = "sync_phase_apply_local_entries" ,
SYNC_PHASE_LIST_REMOTE = 7 ,
SYNC_PHASE_PUSH_ENTRIES = "sync_phase_push_entries" ,
SYNC_PHASE_COMPARE_LOCAL_REMOTE = 8 ,
SYNC_PHASE_POST_PROCESSING = "sync_phase_post_processing" ,
SYNC_PHASE_DOWNLOAD_REMOTE = 9 ,
SYNC_PHASE_SAVE_SYNC_TAG = "sync_phase_save_sync_tag" ;
SYNC_PHASE_POST_PROCESSING = 10 ,
SYNC_PHASE_SAVE_SYNC_STATE = 11 ;
protected final NotificationManagerCompat notificationManager ;
protected final NotificationManagerCompat notificationManager ;
protected final String uniqueCollectionId ;
protected final String uniqueCollectionId ;
@ -81,23 +74,28 @@ abstract public class SyncManager {
protected LocalCollection localCollection ;
protected LocalCollection localCollection ;
protected OkHttpClient httpClient ;
protected OkHttpClient httpClient ;
protected HttpUrl collectionURL ;
protected DavResource davCollection ;
protected JournalEntryManager journal ;
/** remote CTag at the time of {@link #listRemote()} */
/ * *
* remote CTag ( uuid of the last entry on the server ) . We update it when we fetch / push and save when everything works .
* /
protected String remoteCTag = null ;
protected String remoteCTag = null ;
/** sync-able resources in the local collection, as enumerated by {@link #listLocal()} */
/ * *
protected Map < String , LocalResource > localResources ;
* Syncable local journal entries .
* /
/** sync-able resources in the remote collection, as enumerated by {@link #listRemote()} */
protected List < JournalEntryManager . Entry > localEntries ;
protected Map < String , DavResource > remoteResources ;
/** resources which have changed on the server, as determined by {@link #compareLocalRemote()} */
protected Set < DavResource > toDownload ;
/ * *
* Syncable remote journal entries ( fetch from server ) .
* /
protected List < JournalEntryManager . Entry > remoteEntries ;
/ * *
* sync - able resources in the local collection , as enumerated by { @link # prepareLocal ( ) }
* /
protected Map < String , LocalResource > localResources ;
public SyncManager ( Context context , Account account , AccountSettings settings , Bundle extras , String authority , SyncResult syncResult , String uniqueCollectionId ) throws InvalidAccountException {
public SyncManager ( Context context , Account account , AccountSettings settings , Bundle extras , String authority , SyncResult syncResult , String uniqueCollectionId ) throws InvalidAccountException {
this . context = context ;
this . context = context ;
@ -117,95 +115,100 @@ abstract public class SyncManager {
}
}
protected abstract int notificationId ( ) ;
protected abstract int notificationId ( ) ;
protected abstract String getSyncErrorTitle ( ) ;
protected abstract String getSyncErrorTitle ( ) ;
@TargetApi ( 21 )
@TargetApi ( 21 )
public void performSync ( ) {
public void performSync ( ) {
int syncPhase = SYNC_PHASE_PREPARE ;
String syncPhase = SYNC_PHASE_PREPARE ;
try {
try {
App . log . info ( " Preparing synchronization" ) ;
App . log . info ( " Sync phase: " + syncPhase ) ;
prepare ( ) ;
prepare ( ) ;
if ( Thread . interrupted ( ) )
if ( Thread . interrupted ( ) )
return ;
return ;
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES ;
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES ;
App . log . info ( " Querying capabilities" ) ;
App . log . info ( " Sync phase: " + syncPhase ) ;
queryCapabilities ( ) ;
queryCapabilities ( ) ;
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED ;
if ( Thread . interrupted ( ) )
App . log . info ( "Processing locally deleted entries" ) ;
return ;
processLocallyDeleted ( ) ;
syncPhase = SYNC_PHASE_PREPARE_LOCAL ;
App . log . info ( "Sync phase: " + syncPhase ) ;
prepareLocal ( ) ;
/* Create journal entries out of local changes. */
if ( Thread . interrupted ( ) )
if ( Thread . interrupted ( ) )
return ;
return ;
syncPhase = SYNC_PHASE_PREPARE_DIRTY ;
syncPhase = SYNC_PHASE_CREATE_LOCAL_ENTRIES ;
App . log . info ( "Locally preparing dirty entries" ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
prepareDirty ( ) ;
createLocalEntries ( ) ;
syncPhase = SYNC_PHASE_UPLOAD_DIRTY ;
if ( Thread . interrupted ( ) )
App . log . info ( "Uploading dirty entries" ) ;
return ;
uploadDirty ( ) ;
syncPhase = SYNC_PHASE_FETCH_ENTRIES ;
App . log . info ( "Sync phase: " + syncPhase ) ;
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE ;
fetchEntries ( ) ;
App . log . info ( "Checking sync state" ) ;
if ( checkSyncState ( ) ) {
if ( Thread . interrupted ( ) )
syncPhase = SYNC_PHASE_LIST_LOCAL ;
return ;
App . log . info ( "Listing local entries" ) ;
syncPhase = SYNC_PHASE_APPLY_REMOTE_ENTRIES ;
listLocal ( ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
applyRemoteEntries ( ) ;
if ( Thread . interrupted ( ) )
return ;
if ( Thread . interrupted ( ) )
syncPhase = SYNC_PHASE_LIST_REMOTE ;
return ;
App . log . info ( "Listing remote entries" ) ;
syncPhase = SYNC_PHASE_APPLY_LOCAL_ENTRIES ;
listRemote ( ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
applyLocalEntries ( ) ;
if ( Thread . interrupted ( ) )
return ;
if ( Thread . interrupted ( ) )
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE ;
return ;
App . log . info ( "Comparing local/remote entries" ) ;
syncPhase = SYNC_PHASE_PUSH_ENTRIES ;
compareLocalRemote ( ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
pushEntries ( ) ;
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE ;
App . log . info ( "Downloading remote entries" ) ;
/* Cleanup and finalize changes */
downloadRemote ( ) ;
if ( Thread . interrupted ( ) )
return ;
syncPhase = SYNC_PHASE_POST_PROCESSING ;
syncPhase = SYNC_PHASE_POST_PROCESSING ;
App . log . info ( "Post-processing" ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
postProcess ( ) ;
postProcess ( ) ;
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE ;
syncPhase = SYNC_PHASE_SAVE_SYNC_TAG ;
App . log . info ( "Saving sync state" ) ;
App . log . info ( "Sync phase: " + syncPhase ) ;
saveSyncState ( ) ;
saveSyncTag ( ) ;
} else
App . log . info ( "Remote collection didn't change, skipping remote sync" ) ;
} catch ( IOException e ) {
} catch ( IOException | ServiceUnavailableException e ) {
App . log . log ( Level . WARNING , "I/O exception during sync, trying again later" , e ) ;
App . log . log ( Level . WARNING , "I/O exception during sync, trying again later" , e ) ;
syncResult . stats . numIoExceptions + + ;
syncResult . stats . numIoExceptions + + ;
if ( e instanceof ServiceUnavailableException ) {
} catch ( Exceptions . ServiceUnavailableException e ) {
Date retryAfter = ( ( ServiceUnavailableException ) e ) . retryAfter ;
Date retryAfter = null ; // ((Exceptions.ServiceUnavailableException) e).retryAfter;
if ( retryAfter ! = null ) {
if ( retryAfter ! = null ) {
// how many seconds to wait? getTime() returns ms, so divide by 1000
// how many seconds to wait? getTime() returns ms, so divide by 1000
syncResult . delayUntil = ( retryAfter . getTime ( ) - new Date ( ) . getTime ( ) ) / 1000 ;
// syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000;
}
}
}
} catch ( Exception | OutOfMemoryError e ) {
} catch ( Exception | OutOfMemoryError e ) {
final int messageString ;
final int messageString ;
if ( e instanceof UnauthorizedException) {
if ( e instanceof Exceptions. UnauthorizedException) {
App . log . log ( Level . SEVERE , "Not authorized anymore" , e ) ;
App . log . log ( Level . SEVERE , "Not authorized anymore" , e ) ;
messageString = R . string . sync_error_unauthorized ;
messageString = R . string . sync_error_unauthorized ;
syncResult . stats . numAuthExceptions + + ;
syncResult . stats . numAuthExceptions + + ;
} else if ( e instanceof HttpException | | e instanceof Dav Exception) {
} else if ( e instanceof Exceptions. Http Exception) {
App . log . log ( Level . SEVERE , "HTTP /DAV Exception during sync", e ) ;
App . log . log ( Level . SEVERE , "HTTP Exception during sync", e ) ;
messageString = R . string . sync_error_http_dav ;
messageString = R . string . sync_error_http_dav ;
syncResult . stats . numParseExceptions + + ;
syncResult . stats . numParseExceptions + + ;
} else if ( e instanceof CalendarStorageException | | e instanceof ContactsStorageException ) {
} else if ( e instanceof CalendarStorageException | | e instanceof ContactsStorageException ) {
App . log . log ( Level . SEVERE , "Couldn't access local storage" , e ) ;
App . log . log ( Level . SEVERE , "Couldn't access local storage" , e ) ;
messageString = R . string . sync_error_local_storage ;
messageString = R . string . sync_error_local_storage ;
syncResult . databaseError = true ;
syncResult . databaseError = true ;
} else if ( e instanceof Exceptions . IntegrityException ) {
App . log . log ( Level . SEVERE , "Integrity error" , e ) ;
// FIXME: Make a proper error message
messageString = R . string . sync_error ;
syncResult . stats . numParseExceptions + + ;
} else {
} else {
App . log . log ( Level . SEVERE , "Unknown sync error" , e ) ;
App . log . log ( Level . SEVERE , "Unknown sync error" , e ) ;
messageString = R . string . sync_error ;
messageString = R . string . sync_error ;
@ -213,7 +216,7 @@ abstract public class SyncManager {
}
}
final Intent detailsIntent ;
final Intent detailsIntent ;
if ( e instanceof UnauthorizedException) {
if ( e instanceof Exceptions. UnauthorizedException) {
detailsIntent = new Intent ( context , AccountSettingsActivity . class ) ;
detailsIntent = new Intent ( context , AccountSettingsActivity . class ) ;
detailsIntent . putExtra ( AccountSettingsActivity . EXTRA_ACCOUNT , account ) ;
detailsIntent . putExtra ( AccountSettingsActivity . EXTRA_ACCOUNT , account ) ;
} else {
} else {
@ -228,206 +231,162 @@ abstract public class SyncManager {
detailsIntent . setData ( Uri . parse ( "uri://" + getClass ( ) . getName ( ) + "/" + uniqueCollectionId ) ) ;
detailsIntent . setData ( Uri . parse ( "uri://" + getClass ( ) . getName ( ) + "/" + uniqueCollectionId ) ) ;
NotificationCompat . Builder builder = new NotificationCompat . Builder ( context ) ;
NotificationCompat . Builder builder = new NotificationCompat . Builder ( context ) ;
builder . setSmallIcon ( R . drawable . ic_error_light )
builder . setSmallIcon ( R . drawable . ic_error_light )
. setLargeIcon ( App . getLauncherBitmap ( context ) )
. setLargeIcon ( App . getLauncherBitmap ( context ) )
. setContentTitle ( getSyncErrorTitle ( ) )
. setContentTitle ( getSyncErrorTitle ( ) )
. setContentIntent ( PendingIntent . getActivity ( context , 0 , detailsIntent , PendingIntent . FLAG_CANCEL_CURRENT ) )
. setContentIntent ( PendingIntent . getActivity ( context , 0 , detailsIntent , PendingIntent . FLAG_CANCEL_CURRENT ) )
. setCategory ( NotificationCompat . CATEGORY_ERROR ) ;
. setCategory ( NotificationCompat . CATEGORY_ERROR ) ;
try {
String message = context . getString ( messageString , syncPhase ) ;
String [ ] phases = context . getResources ( ) . getStringArray ( R . array . sync_error_phases ) ;
builder . setContentText ( message ) ;
String message = context . getString ( messageString , phases [ syncPhase ] ) ;
builder . setContentText ( message ) ;
} catch ( IndexOutOfBoundsException ex ) {
// should never happen
}
notificationManager . notify ( uniqueCollectionId , notificationId ( ) , builder . build ( ) ) ;
notificationManager . notify ( uniqueCollectionId , notificationId ( ) , builder . build ( ) ) ;
}
}
}
}
abstract protected void prepare ( ) throws ContactsStorageException ;
abstract protected void prepare ( ) throws ContactsStorageException , CalendarStorageException ;
abstract protected void queryCapabilities( ) throws IOException , HttpException , DavException , CalendarStorageException , ContactsStorage Exception;
abstract protected void processSyncEntry( SyncEntry cEntry ) throws IOException , ContactsStorageException , CalendarStorageException , InvalidCalendar Exception;
/ * *
abstract protected void applyLocalEntries ( ) throws IOException , ContactsStorageException , CalendarStorageException , Exceptions . HttpException ;
* Process locally deleted entries ( DELETE them on the server as well ) .
* Checks Thread . interrupted ( ) before each request to allow quick sync cancellation .
* /
protected void processLocallyDeleted ( ) throws CalendarStorageException , ContactsStorageException {
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
// but only if they don't have changed on the server. Then finally remove them from the local address book.
LocalResource [ ] localList = localCollection . getDeleted ( ) ;
for ( LocalResource local : localList ) {
if ( Thread . interrupted ( ) )
return ;
final String fileName = local . getFileName ( ) ;
protected void queryCapabilities ( ) throws IOException , CalendarStorageException , ContactsStorageException {
if ( ! TextUtils . isEmpty ( fileName ) ) {
App . log . info ( fileName + " has been deleted locally -> deleting from server" ) ;
try {
new DavResource ( httpClient , collectionURL . newBuilder ( ) . addPathSegment ( fileName ) . build ( ) )
. delete ( local . getETag ( ) ) ;
} catch ( IOException | HttpException e ) {
App . log . warning ( "Couldn't delete " + fileName + " from server; ignoring (may be downloaded again)" ) ;
}
} else
App . log . info ( "Removing local record #" + local . getId ( ) + " which has been deleted locally and was never uploaded" ) ;
local . delete ( ) ;
syncResult . stats . numDeletes + + ;
}
}
}
protected void prepareDirty ( ) throws CalendarStorageException , ContactsStorageException {
protected void fetchEntries ( ) throws Exceptions . HttpException , ContactsStorageException , CalendarStorageException , Exceptions . IntegrityException {
// assign file names and UIDs to new contacts so that we can use the file name as an index
remoteEntries = journal . getEntries ( settings . password ( ) , remoteCTag ) ;
App . log . info ( "Looking for contacts/groups without file name" ) ;
for ( LocalResource local : localCollection . getWithoutFileName ( ) ) {
if ( ! remoteEntries . isEmpty ( ) ) {
String uuid = UUID . randomUUID ( ) . toString ( ) ;
remoteCTag = remoteEntries . get ( remoteEntries . size ( ) - 1 ) . getUuid ( ) ;
App . log . fine ( "Found local record #" + local . getId ( ) + " without file name; assigning file name/UID based on " + uuid ) ;
local . updateFileNameAndUID ( uuid ) ;
}
}
}
}
abstract protected RequestBody prepareUpload ( LocalResource resource ) throws IOException , CalendarStorageException , ContactsStorageException ;
protected void applyRemoteEntries ( ) throws IOException , ContactsStorageException , CalendarStorageException , InvalidCalendarException {
// Process new vcards from server
/ * *
for ( JournalEntryManager . Entry entry : remoteEntries ) {
* Uploads dirty records to the server , using a PUT request for each record .
* Checks Thread . interrupted ( ) before each request to allow quick sync cancellation .
* /
protected void uploadDirty ( ) throws IOException , HttpException , CalendarStorageException , ContactsStorageException {
// upload dirty contacts
for ( LocalResource local : localCollection . getDirty ( ) ) {
if ( Thread . interrupted ( ) )
if ( Thread . interrupted ( ) )
return ;
return ;
final String fileName = local . getFileName ( ) ;
App . log . info ( "Processing " + entry . toString ( ) ) ;
DavResource remote = new DavResource ( httpClient , collectionURL . newBuilder ( ) . addPathSegment ( fileName ) . build ( ) ) ;
SyncEntry cEntry = SyncEntry . fromJournalEntry ( settings . password ( ) , entry ) ;
App . log . info ( "Processing resource for journal entry " + entry . getUuid ( ) ) ;
processSyncEntry ( cEntry ) ;
}
}
// generate entity to upload (VCard, iCal, whatever)
protected void pushEntries ( ) throws Exceptions . HttpException , IOException , ContactsStorageException , CalendarStorageException {
RequestBody body = prepareUpload ( local ) ;
// upload dirty contacts
// FIXME: Deal with failure
if ( ! localEntries . isEmpty ( ) ) {
journal . putEntries ( localEntries , remoteCTag ) ;
try {
for ( LocalResource local : localCollection . getDirty ( ) ) {
if ( local . getETag ( ) = = null ) {
App . log . info ( "Added/changed resource with UUID: " + local . getUuid ( ) ) ;
App . log . info ( "Uploading new record " + fileName ) ;
local . clearDirty ( local . getUuid ( ) ) ;
remote . put ( body , null , true ) ;
} else {
App . log . info ( "Uploading locally modified record " + fileName ) ;
remote . put ( body , local . getETag ( ) , false ) ;
}
} catch ( ConflictException | PreconditionFailedException e ) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
App . log . log ( Level . INFO , "Resource has been modified on the server before upload, ignoring" , e ) ;
}
}
String eTag = null ;
for ( LocalResource local : localCollection . getDeleted ( ) ) {
GetETag newETag = ( GetETag ) remote . properties . get ( GetETag . NAME ) ;
local . delete ( ) ;
if ( newETag ! = null ) {
}
eTag = newETag . eTag ;
App . log . fine ( "Received new ETag=" + eTag + " after uploading" ) ;
} else
App . log . fine ( "Didn't receive new ETag after uploading, setting to null" ) ;
local. clearDirty ( eTag ) ;
remoteCTag = localEntries . get ( localEntries . size ( ) - 1 ) . getUuid ( ) ;
}
}
}
}
/ * *
protected void createLocalEntries ( ) throws CalendarStorageException , ContactsStorageException , IOException {
* Checks the current sync state ( e . g . CTag ) and whether synchronization from remote is required .
localEntries = new LinkedList < > ( ) ;
* @return < ul >
* < li > < code > true < / code > if the remote collection has changed , i . e . synchronization from remote is required < / li >
// Not saving, just creating a fake one until we load it from a local db
* < li > < code > false < / code > if the remote collection hasn ' t changed < / li >
JournalEntryManager . Entry previousEntry = ( remoteCTag ! = null ) ? JournalEntryManager . Entry . getFakeWithUid ( remoteCTag ) : null ;
* < / ul >
* /
for ( LocalResource local : processLocallyDeleted ( ) ) {
protected boolean checkSyncState ( ) throws CalendarStorageException , ContactsStorageException {
SyncEntry entry = new SyncEntry ( local . getContent ( ) , SyncEntry . Actions . DELETE ) ;
// check CTag (ignore on manual sync)
JournalEntryManager . Entry tmp = new JournalEntryManager . Entry ( ) ;
GetCTag getCTag = ( GetCTag ) davCollection . properties . get ( GetCTag . NAME ) ;
tmp . update ( settings . password ( ) , entry . toJson ( ) , previousEntry ) ;
if ( getCTag ! = null )
previousEntry = tmp ;
remoteCTag = getCTag . cTag ;
localEntries . add ( previousEntry ) ;
}
String localCTag = null ;
if ( extras . containsKey ( ContentResolver . SYNC_EXTRAS_MANUAL ) )
try {
App . log . info ( "Manual sync, ignoring CTag" ) ;
for ( LocalResource local : localCollection . getDirty ( ) ) {
else
SyncEntry . Actions action ;
localCTag = localCollection . getCTag ( ) ;
if ( local . isLocalOnly ( ) ) {
action = SyncEntry . Actions . ADD ;
if ( remoteCTag ! = null & & remoteCTag . equals ( localCTag ) ) {
} else {
App . log . info ( "Remote collection didn't change (CTag=" + remoteCTag + "), no need to query children" ) ;
action = SyncEntry . Actions . CHANGE ;
return false ;
}
} else
return true ;
SyncEntry entry = new SyncEntry ( local . getContent ( ) , action ) ;
JournalEntryManager . Entry tmp = new JournalEntryManager . Entry ( ) ;
tmp . update ( settings . password ( ) , entry . toJson ( ) , previousEntry ) ;
previousEntry = tmp ;
localEntries . add ( previousEntry ) ;
}
} catch ( FileNotFoundException e ) {
// FIXME: Do something
e . printStackTrace ( ) ;
}
}
}
/ * *
/ * *
* Lists all local resources which should be taken into account for synchronization into { @link # localResources } .
* Lists all local resources which should be taken into account for synchronization into { @link # localResources } .
* /
* /
protected void listLocal ( ) throws CalendarStorageException , ContactsStorageException {
protected void prepareLocal ( ) throws CalendarStorageException , ContactsStorageException {
prepareDirty ( ) ;
// fetch list of local contacts and build hash table to index file name
// fetch list of local contacts and build hash table to index file name
LocalResource [ ] localList = localCollection . getAll ( ) ;
LocalResource [ ] localList = localCollection . getAll ( ) ;
localResources = new HashMap < > ( localList . length ) ;
localResources = new HashMap < > ( localList . length ) ;
for ( LocalResource resource : localList ) {
for ( LocalResource resource : localList ) {
App . log . fine ( "Found local resource: " + resource . get FileName ( ) ) ;
App . log . fine ( "Found local resource: " + resource . get Uuid ( ) ) ;
localResources . put ( resource . get FileName ( ) , resource ) ;
localResources . put ( resource . get Uuid ( ) , resource ) ;
}
}
remoteCTag = localCollection . getCTag ( ) ;
}
}
/ * *
* Lists all members of the remote collection which should be taken into account for synchronization into { @link # remoteResources } .
* /
abstract protected void listRemote ( ) throws IOException , HttpException , DavException ;
/ * *
/ * *
* Compares { @link # localResources } and { @link # remoteResources } by file name and ETag :
* Delete unpublished locally deleted , and return the rest .
* < ul >
* Checks Thread . interrupted ( ) before each request to allow quick sync cancellation .
* < li > Local resources which are not available in the remote collection ( anymore ) will be removed . < / li >
* < li > Resources whose remote ETag has changed will be added into { @link # toDownload } < / li >
* < / ul >
* /
* /
protected void compareLocalRemote ( ) throws IOException , HttpException , DavException , CalendarStorageException , ContactsStorageException {
protected List < LocalResource > processLocallyDeleted ( ) throws CalendarStorageException , ContactsStorageException {
/ * check which contacts
// FIXME: This needs refactoring and fixing, it's just not true.
1. are not present anymore remotely - > delete immediately on local side
// Remove locally deleted entries from server (if they have a name, i.e. if they were uploaded before),
2. updated remotely - > add to downloadNames
// but only if they don't have changed on the server. Then finally remove them from the local address book.
3. added remotely - > add to downloadNames
LocalResource [ ] localList = localCollection . getDeleted ( ) ;
* /
List < LocalResource > ret = new ArrayList < > ( localList . length ) ;
toDownload = new HashSet < > ( ) ;
for ( String localName : localResources . keySet ( ) ) {
for ( LocalResource local : localList ) {
DavResource remote = remoteResources . get ( localName ) ;
if ( Thread . interrupted ( ) )
if ( remote = = null ) {
return ret ;
App . log . info ( localName + " is not on server anymore, deleting" ) ;
localResources . get ( localName ) . delete ( ) ;
syncResult . stats . numDeletes + + ;
} else {
// contact is still on server, check whether it has been updated remotely
GetETag getETag = ( GetETag ) remote . properties . get ( GetETag . NAME ) ;
if ( getETag = = null | | getETag . eTag = = null )
throw new DavException ( "Server didn't provide ETag" ) ;
String localETag = localResources . get ( localName ) . getETag ( ) ,
remoteETag = getETag . eTag ;
if ( remoteETag . equals ( localETag ) )
syncResult . stats . numSkippedEntries + + ;
else {
App . log . info ( localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")" ) ;
toDownload . add ( remote ) ;
}
// remote entry has been seen, remove from list
if ( ! local . isLocalOnly ( ) ) {
remoteResources . remove ( localName ) ;
App . log . info ( local . getUuid ( ) + " has been deleted locally -> deleting from server" ) ;
ret . add ( local ) ;
} else {
App . log . info ( "Removing local record #" + local . getId ( ) + " which has been deleted locally and was never uploaded" ) ;
local . delete ( ) ;
}
}
}
// add all unseen (= remotely added) remote contacts
syncResult . stats . numDeletes + + ;
if ( ! remoteResources . isEmpty ( ) ) {
App . log . info ( "New resources have been found on the server: " + TextUtils . join ( ", " , remoteResources . keySet ( ) ) ) ;
toDownload . addAll ( remoteResources . values ( ) ) ;
}
}
return ret ;
}
}
/ * *
protected void prepareDirty ( ) throws CalendarStorageException , ContactsStorageException {
* Downloads the remote resources in { @link # toDownload } and stores them locally .
// assign file names and UIDs to new contacts so that we can use the file name as an index
* Must check Thread . interrupted ( ) periodically to allow quick sync cancellation .
App . log . info ( "Looking for contacts/groups without file name" ) ;
* /
for ( LocalResource local : localCollection . getWithoutFileName ( ) ) {
abstract protected void downloadRemote ( ) throws IOException , HttpException , DavException , ContactsStorageException , CalendarStorageException ;
String uuid = UUID . randomUUID ( ) . toString ( ) ;
App . log . fine ( "Found local record #" + local . getId ( ) + " without file name; assigning file name/UID based on " + uuid ) ;
local . updateFileNameAndUID ( uuid ) ;
}
}
/ * *
/ * *
* For post - processing of entries , for instance assigning groups .
* For post - processing of entries , for instance assigning groups .
@ -435,12 +394,54 @@ abstract public class SyncManager {
protected void postProcess ( ) throws CalendarStorageException , ContactsStorageException {
protected void postProcess ( ) throws CalendarStorageException , ContactsStorageException {
}
}
protected void saveSyncState ( ) throws CalendarStorageException , ContactsStorageException {
protected void saveSyncTag ( ) throws CalendarStorageException , ContactsStorageException {
/ * Save sync state ( CTag ) . It doesn ' t matter if it has changed during the sync process
( for instance , because another client has uploaded changes ) , because this will simply
cause all remote entries to be listed at the next sync . * /
App . log . info ( "Saving CTag=" + remoteCTag ) ;
App . log . info ( "Saving CTag=" + remoteCTag ) ;
localCollection . setCTag ( remoteCTag ) ;
localCollection . setCTag ( remoteCTag ) ;
}
}
static class SyncEntry {
@Getter
private String content ;
@Getter
private Actions action ;
enum Actions {
ADD ( "ADD" ) ,
CHANGE ( "CHANGE" ) ,
DELETE ( "DELETE" ) ;
private final String text ;
Actions ( final String text ) {
this . text = text ;
}
@Override
public String toString ( ) {
return text ;
}
}
@SuppressWarnings ( "unused" )
private SyncEntry ( ) {
}
protected SyncEntry ( String content , Actions action ) {
this . content = content ;
this . action = action ;
}
boolean isAction ( Actions action ) {
return this . action . equals ( action ) ;
}
static SyncEntry fromJournalEntry ( String keyBase64 , JournalEntryManager . Entry entry ) {
return GsonHelper . gson . fromJson ( entry . getContent ( keyBase64 ) , SyncEntry . class ) ;
}
String toJson ( ) {
return GsonHelper . gson . toJson ( this , this . getClass ( ) ) ;
}
}
}
}