2 * Copyright (C) 2011 Morphoss Ltd
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package com
.morphoss
.acal
.service
;
21 import java
.io
.BufferedReader
;
22 import java
.io
.IOException
;
23 import java
.io
.InputStream
;
24 import java
.io
.InputStreamReader
;
25 import java
.util
.ArrayList
;
26 import java
.util
.HashMap
;
27 import java
.util
.List
;
30 import java
.util
.regex
.Matcher
;
32 import javax
.net
.ssl
.SSLException
;
34 import org
.apache
.http
.Header
;
36 import android
.app
.ActivityManager
;
37 import android
.content
.ContentQueryMap
;
38 import android
.content
.ContentResolver
;
39 import android
.content
.ContentValues
;
40 import android
.content
.Context
;
41 import android
.database
.Cursor
;
42 import android
.net
.ConnectivityManager
;
43 import android
.net
.NetworkInfo
;
44 import android
.net
.Uri
;
45 import android
.os
.Debug
;
46 import android
.util
.Log
;
48 import com
.morphoss
.acal
.Constants
;
49 import com
.morphoss
.acal
.DatabaseChangedEvent
;
50 import com
.morphoss
.acal
.HashCodeUtil
;
51 import com
.morphoss
.acal
.ResourceModification
;
52 import com
.morphoss
.acal
.StaticHelpers
;
53 import com
.morphoss
.acal
.acaltime
.AcalDateTime
;
54 import com
.morphoss
.acal
.providers
.DavCollections
;
55 import com
.morphoss
.acal
.providers
.DavResources
;
56 import com
.morphoss
.acal
.providers
.Servers
;
57 import com
.morphoss
.acal
.service
.SynchronisationJobs
.WriteActions
;
58 import com
.morphoss
.acal
.service
.connector
.AcalRequestor
;
59 import com
.morphoss
.acal
.service
.connector
.ConnectionFailedException
;
60 import com
.morphoss
.acal
.service
.connector
.SendRequestFailedException
;
61 import com
.morphoss
.acal
.xml
.DavNode
;
62 import com
.morphoss
.acal
.xml
.DomDavNode
;
63 import com
.morphoss
.acal
.xml
.DomDavXmlTreeBuilder
;
65 public class SyncCollectionContents
extends ServiceJob
{
67 public static final String TAG
= "aCal SyncCollectionContents";
68 private static final int nPerMultiget
= 30;
70 private int collectionId
= -5;
71 private int serverId
= -5;
72 private String collectionPath
= null;
73 private String syncToken
= null;
74 private String oldSyncToken
= null;
75 private boolean isAddressbook
= false;
77 ContentValues collectionData
;
78 private boolean collectionChanged
= false;
79 ContentValues serverData
;
81 private String dataType
= "calendar";
82 private String multigetReportTag
= "calendar-multiget";
83 private String nameSpace
= Constants
.NS_CALDAV
;
85 // Note that this defines how often we wake up and see if we should be
86 // doing a sync. Not how often we actually wake up and hit the server
87 // with a request. Nevertheless we should not do this more than every
88 // minute or so in production.
89 private static final long minBetweenSyncs
= (Constants
.LOG_DEBUG
|| Constants
.debugHeap ?
30000 : 300000); // milliseconds
91 private ContentResolver cr
;
92 private aCalService context
;
93 private boolean synchronisationForced
= false;
94 private AcalRequestor requestor
= null;
95 private boolean resourcesWereSynchronized
;
96 private boolean syncWasCompleted
;
104 * @param collectionId2
106 * The ID of the collection to be synced
110 * The context to use for all those things contexts are used for.
113 public SyncCollectionContents(int collectionId
) {
114 this.collectionId
= collectionId
;
115 this.TIME_TO_EXECUTE
= 0;
121 * Schedule a sync of the contents of a collection, potentially forcing it to happen now even
122 * if this would otherwise be considered too early according to the normal schedule.
124 * @param collectionId
127 public SyncCollectionContents(int collectionId
, boolean forceSync
) {
128 this.collectionId
= collectionId
;
129 this.synchronisationForced
= forceSync
;
130 this.TIME_TO_EXECUTE
= 0;
135 public void run(aCalService context
) {
136 StaticHelpers
.setContext(context
);
137 if ( Constants
.debugHeap
) StaticHelpers
.heapDebug(TAG
, "SyncCollectionContents: start");
138 this.context
= context
;
139 this.cr
= context
.getContentResolver();
140 if ( collectionId
< 0 || !getCollectionInfo()) {
141 Log
.w(TAG
, "Could not read collection " + collectionId
+ " for server " + serverId
142 + " from collection table!");
146 if (!(1 == serverData
.getAsInteger(Servers
.ACTIVE
))) {
147 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Server is no longer active - sync cancelled: " + serverData
.getAsInteger(Servers
.ACTIVE
)
148 + " " + serverData
.getAsString(Servers
.FRIENDLY_NAME
));
152 long start
= System
.currentTimeMillis();
154 resourcesWereSynchronized
= false;
155 syncWasCompleted
= true;
157 // step 1 are there any 'needs_sync' in dav_resources?
158 Map
<String
, ContentValues
> originalData
= findSyncNeededResources();
160 if ( originalData
.size() < 1 && ! timeToRun() ) {
161 scheduleNextInstance();
165 if ( Constants
.LOG_DEBUG
)
166 Log
.d(TAG
, "Starting sync on collection " + this.collectionPath
+ " (" + this.collectionId
+ ")");
168 aCalService
.databaseDispatcher
.dispatchEvent(new DatabaseChangedEvent(
169 DatabaseChangedEvent
.DATABASE_BEGIN_RESOURCE_CHANGES
, DavCollections
.class, collectionData
));
171 if (originalData
.size() < 1) {
172 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "No local resources marked as needing synchronisation.");
175 syncMarkedResources( originalData
);
178 if ( serverData
.getAsInteger(Servers
.HAS_SYNC
) != null && (1 == serverData
.getAsInteger(Servers
.HAS_SYNC
))) {
179 if (doRegularSyncReport()) {
180 originalData
= findSyncNeededResources();
181 syncMarkedResources(originalData
);
185 if (doRegularSyncPropfind()) {
186 originalData
= findSyncNeededResources();
187 syncMarkedResources(originalData
);
191 String lastSynchronized
= new AcalDateTime().setMillis(start
).fmtIcal();
192 if ( syncWasCompleted
) {
193 // update last checked flag for collection
194 collectionData
.put(DavCollections
.LAST_SYNCHRONISED
, lastSynchronized
);
195 collectionData
.put(DavCollections
.NEEDS_SYNC
, 0);
196 if ( syncToken
!= null ) {
197 collectionData
.put(DavCollections
.SYNC_TOKEN
, syncToken
);
198 if ( Constants
.LOG_VERBOSE
)
199 Log
.i(TAG
,"Updated collection record with new sync token '"+syncToken
+"' at "+lastSynchronized
);
201 cr
.update(DavCollections
.CONTENT_URI
, collectionData
, DavCollections
._ID
+ "=?",
202 new String
[] { "" + collectionId
});
206 if ( resourcesWereSynchronized
) {
207 aCalService
.databaseDispatcher
.dispatchEvent(
208 new DatabaseChangedEvent(DatabaseChangedEvent
.DATABASE_RECORD_UPDATED
,
209 DavCollections
.class, collectionData
)
213 catch (Exception e
) {
214 Log
.e(TAG
, "Error syncing collection " + this.collectionId
+ ": " + e
.getMessage());
215 Log
.e(TAG
, Log
.getStackTraceString(e
));
218 aCalService
.databaseDispatcher
.dispatchEvent(new DatabaseChangedEvent(
219 DatabaseChangedEvent
.DATABASE_END_RESOURCE_CHANGES
, DavCollections
.class, collectionData
));
221 long finish
= System
.currentTimeMillis();
222 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Collection sync finished in " + (finish
- start
) + "ms");
224 scheduleNextInstance();
226 this.collectionData
= null;
227 this.serverData
= null;
229 this.requestor
= null;
231 if ( Constants
.debugHeap
) StaticHelpers
.heapDebug(TAG
, "SyncCollectionContents: end");
236 * Called from the constructor to initialise all of the collection-related data we need to be able to sync
237 * properly. This includes a bunch of server related data pulled into serverData
240 * @return Whether it successfully read enough data to proceed
242 private boolean getCollectionInfo() {
243 long start
= System
.currentTimeMillis();
244 collectionData
= DavCollections
.getRow(collectionId
, cr
);
245 if ( collectionData
== null ) {
246 Log
.e(TAG
, "Error getting collection data from DB for collection ID " + collectionId
);
250 serverId
= collectionData
.getAsInteger(DavCollections
.SERVER_ID
);
251 collectionPath
= collectionData
.getAsString(DavCollections
.COLLECTION_PATH
);
252 oldSyncToken
= collectionData
.getAsString(DavCollections
.SYNC_TOKEN
);
253 isAddressbook
= (1 == collectionData
.getAsInteger(DavCollections
.ACTIVE_ADDRESSBOOK
));
254 dataType
= "calendar";
255 multigetReportTag
= "calendar-multiget";
256 nameSpace
= Constants
.NS_CALDAV
;
258 dataType
= "address";
259 multigetReportTag
= dataType
+ "book-multiget";
260 nameSpace
= Constants
.NS_CARDDAV
;
265 serverData
= Servers
.getRow(serverId
, cr
);
266 if (serverData
== null) throw new Exception("No record for ID " + serverId
);
267 requestor
= AcalRequestor
.fromServerValues(serverData
);
268 requestor
.setPath(collectionPath
);
270 catch (Exception e
) {
271 // Error getting data
272 Log
.e(TAG
, "Error getting server data: " + e
.getMessage());
273 Log
.e(TAG
, "Deleting invalid collection Record.");
274 cr
.delete(Uri
.withAppendedPath(DavCollections
.CONTENT_URI
,Long
.toString(collectionId
)), null, null);
278 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "getCollectionInfo() completed in " + (System
.currentTimeMillis() - start
) + "ms");
285 * Do a sync run using a sync-collection REPORT against the collection, hopefully retrieving the -data
286 * pseudo-properties at the same time, but in any case getting a list of changed resources to process.
287 * Quick and light on the bandwidth, we hope.
290 * @return true if we still need to syncMarkedResources() afterwards.
292 private boolean doRegularSyncReport() {
293 DavNode root
= doCalendarRequest("REPORT", 1,
294 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
295 + "<sync-collection xmlns=\"DAV:\">"
298 // + "<getlastmodified/>"
299 // + "<" + dataType + "-data xmlns=\"" + nameSpace + "\"/>"
301 + "<sync-token>" + oldSyncToken
+ "</sync-token>"
302 + "</sync-collection>"
305 boolean needSyncAfterwards
= false;
308 Log
.i(TAG
, "Unable to sync collection " + this.collectionPath
+ " (ID:" + this.collectionId
309 + " - no data from server.");
310 syncWasCompleted
= false;
314 ArrayList
<ResourceModification
> changeList
= new ArrayList
<ResourceModification
>();
316 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Start processing response");
317 List
<DavNode
> responses
= root
.getNodesFromPath("multistatus/response");
319 for (DavNode response
: responses
) {
320 String name
= response
.segmentFromFirstHref("href");
321 WriteActions action
= WriteActions
.UPDATE
;
323 ContentValues cv
= DavResources
.getResourceInCollection( collectionId
, name
, cr
);
325 cv
= new ContentValues();
326 cv
.put(DavResources
.COLLECTION_ID
, collectionId
);
327 cv
.put(DavResources
.RESOURCE_NAME
, name
);
328 cv
.put(DavResources
.NEEDS_SYNC
, 1 );
329 action
= WriteActions
.INSERT
;
331 String oldEtag
= cv
.getAsString(DavResources
.ETAG
);
333 List
<DavNode
> aNode
= response
.getNodesFromPath("status");
335 || aNode
.get(0).getText().equalsIgnoreCase("HTTP/1.1 201 Created")
336 || aNode
.get(0).getText().equalsIgnoreCase("HTTP/1.1 200 OK") ) {
338 Log
.i(TAG
,"Updating node "+name
+" with "+action
.toString() );
339 // We are dealing with an update or insert
340 if ( !parseResponseNode(response
, cv
) ) continue;
341 if ( cv
.getAsInteger(DavResources
.NEEDS_SYNC
) == 1 ) needSyncAfterwards
= true;
343 if ( oldEtag
!= null && cv
.getAsString(DavResources
.ETAG
) != null ) {
344 if ( oldEtag
.equals(cv
.getAsString(DavResources
.ETAG
)) ) {
345 // Resource in both places, but is unchanged.
346 Log
.d(TAG
,"Notified of change to resource but etag already matches!");
347 root
.removeSubTree(response
);
350 if ( Constants
.LOG_DEBUG
)
351 Log
.d(TAG
,"Old etag="+oldEtag
+", new etag="+cv
.getAsString(DavResources
.ETAG
));
354 else if ( action
== WriteActions
.INSERT
) {
355 // It looked like an INSERT because it's not in our DB, but in fact
356 // the status message was not 200/201 so it's a DELETE that we're
357 // seeing reflected back at us.
358 Log
.i(TAG
,"Ignoring delete sync on node '"+name
+"' which is already deleted from our DB." );
361 // This really *is* a DELETE, since the status could only
362 // have said so. Or we're getting invalid status messages
363 // and their events all deserve to die anyway!
364 Log
.i(TAG
,"Deleting node '"+name
+"'with status: "+aNode
.get(0).getText() );
365 action
= WriteActions
.DELETE
;
367 root
.removeSubTree(response
);
369 changeList
.add( new ResourceModification(action
, cv
, null) );
373 // Pull the syncToken we will update with.
374 syncToken
= root
.getFirstNodeText("multistatus/sync-token");
375 Log
.i(TAG
,"Found sync token of '"+syncToken
+"' in sync-report response." );
377 ResourceModification
.commitChangeList(context
, changeList
);
379 return needSyncAfterwards
;
385 * Do a sync run using a PROPFIND against the collection and a pass through the DB comparing all resources
386 * currently on file with the ones we got from the PROPFIND. Laborious and potentially bandwidth hogging.
389 * @return true if we still need to syncMarkedResources() afterwards.
391 private boolean doRegularSyncPropfind() {
392 boolean needSyncAfterwards
= false;
393 if ( !collectionTagChanged() ) return false;
395 DavNode root
= doCalendarRequest("PROPFIND", 1,
396 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
397 + "<propfind xmlns=\"DAV:\" xmlns:CS=\"http://calendarserver.org/ns/\">"
406 Log
.i(TAG
, "Unable to PROPFIND collection " + this.collectionPath
+ " (ID:" + this.collectionId
407 + " - no data from server.");
408 syncWasCompleted
= false;
412 Map
<String
, ContentValues
> ourResourceMap
= getCurrentResourceMap();
413 ArrayList
<ResourceModification
> changeList
= new ArrayList
<ResourceModification
>();
416 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Start processing PROPFIND response");
417 long start2
= System
.currentTimeMillis();
418 List
<DavNode
> responses
= root
.getNodesFromPath("multistatus/response");
420 for (DavNode response
: responses
) {
421 String name
= response
.segmentFromFirstHref("href");
423 ContentValues cv
= null;
424 WriteActions action
= WriteActions
.UPDATE
;
425 if ( ourResourceMap
!= null && ourResourceMap
.containsKey(name
)) {
426 cv
= ourResourceMap
.get(name
);
427 ourResourceMap
.remove(name
);
430 cv
= new ContentValues();
431 cv
.put(DavResources
.COLLECTION_ID
, collectionId
);
432 cv
.put(DavResources
.RESOURCE_NAME
, name
);
433 cv
.put(DavResources
.NEEDS_SYNC
, 1);
434 action
= WriteActions
.INSERT
;
437 if ( !parseResponseNode(response
, cv
) ) continue;
439 needSyncAfterwards
= true;
440 cv
.put(DavResources
.NEEDS_SYNC
, 1);
442 changeList
.add( new ResourceModification(action
, cv
, null) );
445 if ( ourResourceMap
!= null ) {
446 // Delete any records still in ourResourceMap (hence not on server any longer)
447 Set
<String
> names
= ourResourceMap
.keySet();
448 for( String name
: names
) {
449 ContentValues cv
= ourResourceMap
.get(name
);
450 changeList
.add( new ResourceModification(WriteActions
.DELETE
, cv
, null) );
454 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Completed processing of PROPFIND sync in " + (System
.currentTimeMillis() - start2
) + "ms");
456 catch (Exception e
) {
457 Log
.e(TAG
, "Exception processing PROPFIND response: " + e
.getMessage());
458 Log
.e(TAG
, Log
.getStackTraceString(e
));
461 ResourceModification
.commitChangeList(context
, changeList
);
463 return needSyncAfterwards
;
468 * Returns a Map of href to database record for the current database state.
471 private Map
<String
,ContentValues
> getCurrentResourceMap() {
472 Cursor resourceCursor
= cr
.query(Uri
.parse(DavResources
.CONTENT_URI
.toString() + "/collection/" + this.collectionId
),
473 new String
[] { DavResources
._ID
, DavResources
.RESOURCE_NAME
, DavResources
.ETAG
}, null, null, null);
474 if ( !resourceCursor
.moveToFirst()) {
475 resourceCursor
.close();
476 return new HashMap
<String
,ContentValues
>();
478 ContentQueryMap cqm
= new ContentQueryMap(resourceCursor
, DavResources
.RESOURCE_NAME
, false, null);
480 Map
<String
, ContentValues
> databaseList
= cqm
.getRows();
482 resourceCursor
.close();
489 * Checks for an old CalendarServer-style ctag for this collection
493 * Returns true if the CTag is different from the previous one, or if either is null.
496 private boolean collectionTagChanged() {
497 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Requesting CTag on collection.");
498 DavNode root
= doCalendarRequest("PROPFIND", 0,
499 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
500 + "<propfind xmlns=\"DAV:\" xmlns:CS=\"http://calendarserver.org/ns/\">"
507 if ( root
== null ) {
508 Log
.i(TAG
,"No response from server - deferring sync.");
512 List
<DavNode
> responses
= root
.getNodesFromPath("multistatus/response");
514 for (DavNode response
: responses
) {
516 List
<DavNode
> propstats
= response
.getNodesFromPath("propstat");
518 for (DavNode propstat
: propstats
) {
519 if ( !propstat
.getFirstNodeText("status").equalsIgnoreCase("HTTP/1.1 200 OK") )
522 DavNode prop
= propstat
.getNodesFromPath("prop").get(0);
523 String ctag
= prop
.getFirstNodeText("getctag");
524 String collectionTag
= collectionData
.getAsString(DavCollections
.COLLECTION_TAG
);
526 if ( ctag
== null || collectionTag
== null ) return true;
527 return ! ctag
.equals(collectionTag
);
537 * Does a request against the collection path
541 * A DavNode which is the root of the multistatus response.
544 private DavNode
doCalendarRequest( String method
, int depth
, String xml
) {
545 DavNode root
= requestor
.doXmlRequest(method
, collectionPath
,
546 SynchronisationJobs
.getReportHeaders(depth
), xml
);
547 if ( requestor
.getStatusCode() == 404 ) {
548 Log
.i(TAG
,"Sync PROPFIND got 404 on "+collectionPath
+" so a HomeSetsUpdate is being scheduled.");
549 ServiceJob sj
= new HomeSetsUpdate(serverId
);
550 context
.addWorkerJob(sj
);
558 * Checks the resources we have in the DB currently flagged as needing synchronisation, and synchronises
559 * them if they are using an addressbook-multiget or calendar-multiget request, depending on the
562 private void syncMarkedResources( Map
<String
, ContentValues
> originalData
) {
564 if (Constants
.LOG_DEBUG
)
565 Log
.d(TAG
, "Found " + originalData
.size() + " resources marked as needing synchronisation.");
567 Set
<String
> hrefSet
= originalData
.keySet();
568 Object
[] hrefs
= hrefSet
.toArray();
570 if (serverData
.getAsInteger(Servers
.HAS_MULTIGET
) != null && 1 == serverData
.getAsInteger(Servers
.HAS_MULTIGET
)) {
571 syncWithMultiget(originalData
, hrefs
);
574 syncWithGet(originalData
, hrefs
);
580 * Performs a sync using a series of multiget REPORT requests to retrieve the resources needing sync.
583 * @param originalData
585 * The href => Contentvalues map.
592 private void syncWithMultiget(Map
<String
, ContentValues
> originalData
, Object
[] hrefs
) {
594 String baseXml
= "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
595 + "<" + multigetReportTag
+ " xmlns=\"" + nameSpace
+ "\" xmlns:D=\"DAV:\">\n"
598 + "<D:getcontenttype/>\n"
599 + "<D:getlastmodified/>\n"
600 + "<" + dataType
+ "-data/>\n"
603 + "</" + multigetReportTag
+ ">";
605 ArrayList
<String
> toBeRemoved
= new ArrayList
<String
>(hrefs
.length
);
606 for( Object o
: hrefs
) {
607 Matcher m
= Constants
.matchSegmentName
.matcher(o
.toString());
608 if ( m
.find() ) toBeRemoved
.add(m
.group(1));
612 StringBuilder hrefList
;
613 for (int hrefIndex
= 0; hrefIndex
< hrefs
.length
; hrefIndex
+= nPerMultiget
) {
614 limit
= nPerMultiget
+ hrefIndex
;
615 if ( limit
> hrefs
.length
) limit
= hrefs
.length
;
617 hrefList
= new StringBuilder();
618 for (int i
= hrefIndex
; i
< limit
; i
++) {
619 hrefList
.append(String
.format("<D:href>%s</D:href>", collectionPath
+ hrefs
[i
].toString()));
622 if (Constants
.LOG_DEBUG
)
623 Log
.d(TAG
, "Requesting " + multigetReportTag
+ " for " + nPerMultiget
+ " resources out of "+hrefs
.length
+"." );
625 DavNode root
= doCalendarRequest( "REPORT", 1, String
.format(baseXml
,hrefList
.toString()) );
628 Log
.w(TAG
, "Unable to sync collection " + this.collectionPath
+ " (ID:" + this.collectionId
629 + " - no data from server).");
633 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Start processing response");
634 List
<DavNode
> responses
= root
.getNodesFromPath("multistatus/response");
635 List
<ResourceModification
> changeList
= new ArrayList
<ResourceModification
>(hrefList
.length());
637 for (DavNode response
: responses
) {
638 String name
= response
.segmentFromFirstHref("href");
639 if ( toBeRemoved
.contains(name
) ) {
640 Log
.v(TAG
,"Found href in our list.");
641 toBeRemoved
.remove(name
);
644 ContentValues cv
= originalData
.get(name
);
645 WriteActions action
= WriteActions
.UPDATE
;
647 cv
= new ContentValues();
648 cv
.put(DavResources
.COLLECTION_ID
, collectionId
);
649 cv
.put(DavResources
.RESOURCE_NAME
, name
);
650 action
= WriteActions
.INSERT
;
652 if ( !parseResponseNode(response
, cv
) ) continue;
653 if ( cv
.getAsString("COLLECTION") != null ) continue;
655 if (Constants
.LOG_DEBUG
)
656 Log
.d(TAG
, "Multiget response needs sync="+cv
.getAsString(DavResources
.NEEDS_SYNC
)+" for "+name
);
658 changeList
.add( new ResourceModification(action
, cv
, null) );
661 ResourceModification
.commitChangeList(context
, changeList
);
663 if ( hrefIndex
+ nPerMultiget
< hrefs
.length
) {
664 Debug
.MemoryInfo mi
= new Debug
.MemoryInfo();
666 Debug
.getMemoryInfo(mi
);
668 catch ( Exception e
) {
669 Log
.i(TAG
,"Unable to get Debug.MemoryInfo() because: " + e
.getMessage());
671 if ( Constants
.LOG_DEBUG
) {
673 Log
.d(TAG
,String
.format("MemoryInfo: Dalvik(%d,%d,%d), Native(%d,%d,%d), Other(%d,%d,%d)",
674 mi
.dalvikPrivateDirty
, mi
.dalvikPss
, mi
.dalvikSharedDirty
,
675 mi
.nativePrivateDirty
, mi
.nativePss
, mi
.nativeSharedDirty
,
676 mi
.otherPrivateDirty
, mi
.otherPss
, mi
.otherSharedDirty
) );
679 ActivityManager
.MemoryInfo ammi
= new ActivityManager
.MemoryInfo();
680 if ( ammi
.lowMemory
) {
681 Log
.i(TAG
, "Android thinks we're low memory right now, rescheduling more sync in 30 seconds time." );
682 // Reschedule for another run, rather than continue now.
683 SyncCollectionContents sj
= new SyncCollectionContents(collectionId
,true);
684 sj
.TIME_TO_EXECUTE
= System
.currentTimeMillis() + 30000;
685 context
.addWorkerJob(sj
);
686 syncWasCompleted
= false;
692 for( String href
: toBeRemoved
) {
693 Log
.v(TAG
,"Did not find +"+href
+"+ in the list.");
695 if ( toBeRemoved
.size() > 0 ) {
696 doRegularSyncPropfind();
706 * Finds the resources which have been marked as needing synchronisation in our local database.
709 * @return A map of String/Data which are the hrefs we need to sync
711 private Map
<String
, ContentValues
> findSyncNeededResources() {
712 long start
= System
.currentTimeMillis();
713 Map
<String
, ContentValues
> originalData
= null;
715 // step 1a get list of resources from db
716 start
= System
.currentTimeMillis();
718 Cursor mCursor
= this.cr
.query(
719 Uri
.parse(DavResources
.CONTENT_URI
.toString() + "/collection/" + this.collectionId
),
721 DavResources
.NEEDS_SYNC
+ " = 1 OR "+DavResources
.RESOURCE_DATA
+" IS NULL",
723 ContentQueryMap cqm
= new ContentQueryMap(mCursor
, DavResources
.RESOURCE_NAME
, false, null);
725 originalData
= cqm
.getRows();
727 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "DavCollections ContentQueryMap retrieved in " + (System
.currentTimeMillis() - start
) + "ms");
733 * Parse a single <response> node within a <multistatus>
735 * @return true If we need to write to the database, false otherwise.
737 private boolean parseResponseNode(DavNode responseNode
, ContentValues cv
) {
738 boolean validResourceResponse
= true;
741 for ( DavNode testPs
: responseNode
.getNodesFromPath("propstat")) {
742 String statusText
= testPs
.getFirstNodeText("status");
743 if ( statusText
.equalsIgnoreCase("HTTP/1.1 200 OK") || statusText
.equalsIgnoreCase("HTTP/1.1 201 Created")) {
744 prop
= testPs
.getNodesFromPath("prop").get(0);
749 if ( prop
== null ) {
750 validResourceResponse
= false;
753 String s
= prop
.getFirstNodeText("getctag");
755 collectionChanged
= (collectionData
.getAsString(DavCollections
.COLLECTION_TAG
) == null
756 || s
.equals(collectionData
.getAsString(DavCollections
.COLLECTION_TAG
)));
757 if ( collectionChanged
) collectionData
.put(DavCollections
.COLLECTION_TAG
, s
);
758 validResourceResponse
= false;
759 cv
.put("COLLECTION", true);
762 String etag
= prop
.getFirstNodeText("getetag");
764 if ( etag
!= null ) {
765 String oldEtag
= cv
.getAsString(DavResources
.ETAG
);
766 if ( oldEtag
!= null && oldEtag
.equals(etag
) ) {
767 cv
.put(DavResources
.NEEDS_SYNC
, 0);
768 responseNode
.getParent().removeSubTree(responseNode
);
772 if ( Constants
.LOG_DEBUG
)
773 Log
.d(TAG
,"Etags not equal: old="+oldEtag
+", new="+etag
+", proposing to sync.");
774 cv
.put(DavResources
.NEEDS_SYNC
, 1);
778 String data
= prop
.getFirstNodeText(dataType
+ "-data");
779 if ( data
!= null ) {
780 // if ( Constants.LOG_VERBOSE ) {
781 // Log.v(TAG,"Found data in response:");
783 // Log.v(TAG,StaticHelpers.toHexString(data.substring(0,40).getBytes()));
785 cv
.put(DavResources
.RESOURCE_DATA
, data
);
786 cv
.put(DavResources
.ETAG
, etag
);
787 cv
.put(DavResources
.NEEDS_SYNC
, 0);
789 s
= prop
.getFirstNodeText("getlastmodified");
790 if ( s
!= null ) cv
.put(DavResources
.LAST_MODIFIED
, s
);
792 s
= prop
.getFirstNodeText("getcontenttype");
793 if ( s
!= null ) cv
.put(DavResources
.CONTENT_TYPE
, s
);
798 // Remove our references to this now that we've finished with it.
799 responseNode
.getParent().removeSubTree(responseNode
);
801 return validResourceResponse
;
806 * Performs a sync using a series of GET requests to retrieve each resource needing sync. This is a
807 * fallback strategy and we really expect multiget to work in almost all circumstances.
810 * @param originalData
812 * The href => Contentvalues map.
819 private void syncWithGet(Map
<String
, ContentValues
> originalData
, Object
[] hrefs
) {
820 long fullMethod
= System
.currentTimeMillis();
822 Header
[] headers
= new Header
[] {};
823 List
<ResourceModification
> changeList
= new ArrayList
<ResourceModification
>(hrefs
.length
);
828 for (int hrefIndex
= 0; hrefIndex
< hrefs
.length
; hrefIndex
++) {
829 path
= collectionPath
+ hrefs
[hrefIndex
];
832 in
= requestor
.doRequest("GET", path
, headers
, "");
834 catch (ConnectionFailedException e
) {
835 Log
.i(TAG
,"ConnectionFailedException ("+e
.getMessage()+") on GET from "+path
);
838 catch (SendRequestFailedException e
) {
839 Log
.i(TAG
,"SendRequestFailedException ("+e
.getMessage()+") on GET from "+path
);
842 catch (SSLException e
) {
843 Log
.i(TAG
,"SSLException on GET from "+path
);
847 if (Constants
.LOG_DEBUG
) Log
.d(TAG
, "Error - Unable to get data stream from server.");
851 status
= requestor
.getStatusCode();
853 case 200: // Status O.K.
854 StringBuilder resourceData
= new StringBuilder();
855 BufferedReader r
= new BufferedReader(new InputStreamReader(in
));
858 while ((line
= r
.readLine() ) != null) {
859 resourceData
.append(line
);
860 resourceData
.append("\n");
863 catch (IOException e
) {
864 Log
.i(TAG
,Log
.getStackTraceString(e
));
866 ContentValues cv
= originalData
.get(hrefs
[hrefIndex
]);
867 cv
.put(DavResources
.RESOURCE_DATA
, resourceData
.toString() );
868 for (Header hdr
: requestor
.getResponseHeaders()) {
869 if (hdr
.getName().equalsIgnoreCase("ETag")) {
870 cv
.put(DavResources
.ETAG
, hdr
.getValue());
874 cv
.put(DavResources
.NEEDS_SYNC
, 0);
875 changeList
.add( new ResourceModification(WriteActions
.UPDATE
, cv
, null) );
876 if (Constants
.LOG_DEBUG
)
877 Log
.d(TAG
, "Get response for "+hrefs
[hrefIndex
] );
880 default: // Unknown code
881 Log
.w(TAG
, "Status " + status
+ " on GET request for " + path
);
886 ResourceModification
.commitChangeList(context
, changeList
);
888 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "syncWithGet() for " + hrefs
.length
+ " resources took "
889 + (System
.currentTimeMillis() - fullMethod
) + "ms");
895 private boolean timeToRun() {
896 if ( synchronisationForced
) {
897 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Synchronising now, since a sync has been forced.");
900 String needsSyncNow
= collectionData
.getAsString(DavCollections
.NEEDS_SYNC
);
901 if ( needsSyncNow
== null || needsSyncNow
.equals("1") ) {
902 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Synchronising now, since needs_sync is true.");
905 String lastSyncString
= collectionData
.getAsString(DavCollections
.LAST_SYNCHRONISED
);
906 if ( lastSyncString
== null ) {
907 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Synchronising now, since last_sync is null.");
910 AcalDateTime lastRunTime
= null;
912 lastRunTime
= AcalDateTime
.fromString(lastSyncString
);
913 if ( lastRunTime
== null ) return true;
915 lastRunTime
.applyLocalTimeZone();
917 ConnectivityManager conMan
= (ConnectivityManager
) context
.getSystemService(Context
.CONNECTIVITY_SERVICE
);
918 NetworkInfo netInfo
= conMan
.getActiveNetworkInfo();
919 long maxAgeMs
= minBetweenSyncs
;
920 if ( netInfo
!= null && netInfo
.getType() == ConnectivityManager
.TYPE_MOBILE
)
921 maxAgeMs
= collectionData
.getAsLong(DavCollections
.MAX_SYNC_AGE_3G
);
923 maxAgeMs
= collectionData
.getAsLong(DavCollections
.MAX_SYNC_AGE_WIFI
);
925 if ( maxAgeMs
< minBetweenSyncs
) maxAgeMs
= minBetweenSyncs
;
927 if (Constants
.LOG_VERBOSE
) Log
.v(TAG
, "Considering whether we are " + maxAgeMs
/ 1000 + "s past "
928 + lastRunTime
.fmtIcal() + "("+lastRunTime
.getMillis()+") yet? "
929 + "Now: " + new AcalDateTime().fmtIcal() + "("+System
.currentTimeMillis()+")... So: "
930 + ((maxAgeMs
+ lastRunTime
.getMillis() < System
.currentTimeMillis()) ?
"yes" : "no"));
932 return (maxAgeMs
+ lastRunTime
.getMillis() < System
.currentTimeMillis());
936 private void scheduleNextInstance() {
937 String lastSync
= collectionData
.getAsString(DavCollections
.LAST_SYNCHRONISED
);
938 long timeToWait
= minBetweenSyncs
;
939 if ( lastSync
!= null ) {
940 long maxAge3g
= collectionData
.getAsLong(DavCollections
.MAX_SYNC_AGE_3G
);
941 long maxAgeWifi
= collectionData
.getAsLong(DavCollections
.MAX_SYNC_AGE_WIFI
);
942 timeToWait
= (maxAge3g
> maxAgeWifi ? maxAgeWifi
: maxAge3g
);
944 AcalDateTime lastRunTime
= null;
945 lastRunTime
= AcalDateTime
.fromString(lastSync
);
946 lastRunTime
.applyLocalTimeZone();
947 timeToWait
+= (lastRunTime
.getMillis() - System
.currentTimeMillis());
949 if ( minBetweenSyncs
> timeToWait
) timeToWait
= minBetweenSyncs
;
952 if ( Constants
.LOG_VERBOSE
)
953 Log
.v(TAG
, "Scheduling next sync status check for "+collectionId
+" - '"
954 + collectionData
.getAsString(DavCollections
.DISPLAYNAME
)
955 +"' in " + Long
.toString(timeToWait
/ 1000) + " seconds.");
957 this.TIME_TO_EXECUTE
= timeToWait
;
958 context
.addWorkerJob(this);
961 public boolean equals(Object that
) {
962 if (this == that
) return true;
963 if (!(that
instanceof SyncCollectionContents
)) return false;
964 SyncCollectionContents thatCis
= (SyncCollectionContents
) that
;
965 return this.collectionId
== thatCis
.collectionId
;
968 public int hashCode() {
969 int result
= HashCodeUtil
.SEED
;
970 result
= HashCodeUtil
.hash(result
, this.collectionId
);
975 public String
getDescription() {
976 return "Syncing collection contents of collection " + collectionId
;