Get rid of fakeResponse testing method.
[acal.git] / src / com / morphoss / acal / service / SyncCollectionContents.java
blob71f5963cc8a1c7c9a622fd1a8b2c12ae55c9d57d
1 /*
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;
28 import java.util.Map;
29 import java.util.Set;
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;
99 /**
100 * <p>
101 * Constructor
102 * </p>
104 * @param collectionId2
105 * <p>
106 * The ID of the collection to be synced
107 * </p>
108 * @param context
109 * <p>
110 * The context to use for all those things contexts are used for.
111 * </p>
113 public SyncCollectionContents(int collectionId) {
114 this.collectionId = collectionId;
115 this.TIME_TO_EXECUTE = 0;
120 * <p>
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.
123 * </p>
124 * @param collectionId
125 * @param forceSync
127 public SyncCollectionContents(int collectionId, boolean forceSync ) {
128 this.collectionId = collectionId;
129 this.synchronisationForced = forceSync;
130 this.TIME_TO_EXECUTE = 0;
134 @Override
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!");
143 return;
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));
149 return;
152 long start = System.currentTimeMillis();
153 try {
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();
162 return;
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.");
174 else
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);
184 else {
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));
217 finally {
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;
228 this.context = null;
229 this.requestor = null;
230 this.cr = null;
231 if ( Constants.debugHeap ) StaticHelpers.heapDebug(TAG, "SyncCollectionContents: end");
235 * <p>
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
238 * </p>
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);
247 return false;
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;
257 if (isAddressbook) {
258 dataType = "address";
259 multigetReportTag = dataType + "book-multiget";
260 nameSpace = Constants.NS_CARDDAV;
263 try {
264 // get serverData
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);
275 return false;
278 if (Constants.LOG_VERBOSE) Log.v(TAG, "getCollectionInfo() completed in " + (System.currentTimeMillis() - start) + "ms");
279 return true;
284 * <p>
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.
288 * </p>
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:\">"
296 + "<prop>"
297 + "<getetag/>"
298 // + "<getlastmodified/>"
299 // + "<" + dataType + "-data xmlns=\"" + nameSpace + "\"/>"
300 + "</prop>"
301 + "<sync-token>" + oldSyncToken + "</sync-token>"
302 + "</sync-collection>"
305 boolean needSyncAfterwards = false;
307 if (root == null) {
308 Log.i(TAG, "Unable to sync collection " + this.collectionPath + " (ID:" + this.collectionId
309 + " - no data from server.");
310 syncWasCompleted = false;
311 return 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);
324 if ( cv == null ) {
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");
334 if ( aNode.isEmpty()
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);
348 continue;
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." );
360 else {
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;
384 * <p>
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.
387 * </p>
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/\">"
398 + "<prop>"
399 + "<getetag/>"
400 + "<CS:getctag/>"
401 + "</prop>"
402 + "</propfind>"
405 if (root == null ) {
406 Log.i(TAG, "Unable to PROPFIND collection " + this.collectionPath + " (ID:" + this.collectionId
407 + " - no data from server.");
408 syncWasCompleted = false;
409 return false;
412 Map<String, ContentValues> ourResourceMap = getCurrentResourceMap();
413 ArrayList<ResourceModification> changeList = new ArrayList<ResourceModification>();
415 try {
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);
429 else {
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.
469 * @return
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);
479 cqm.requery();
480 Map<String, ContentValues> databaseList = cqm.getRows();
481 cqm.close();
482 resourceCursor.close();
483 return databaseList;
488 * <p>
489 * Checks for an old CalendarServer-style ctag for this collection
490 * </p>
492 * @return <p>
493 * Returns true if the CTag is different from the previous one, or if either is null.
494 * </p>
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/\">"
501 + "<prop>"
502 + "<CS:getctag/>"
503 + "</prop>"
504 + "</propfind>"
507 if ( root == null ) {
508 Log.i(TAG,"No response from server - deferring sync.");
509 return false;
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") )
520 continue;
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);
530 return true;
536 * <p>
537 * Does a request against the collection path
538 * </p>
540 * @return <p>
541 * A DavNode which is the root of the multistatus response.
542 * </p>
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);
551 return null;
553 return root;
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
560 * collection type.
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);
573 else {
574 syncWithGet(originalData, hrefs);
579 * <p>
580 * Performs a sync using a series of multiget REPORT requests to retrieve the resources needing sync.
581 * </p>
583 * @param originalData
584 * <p>
585 * The href => Contentvalues map.
586 * </p>
587 * @param hrefs
588 * <p>
589 * The array of hrefs
590 * </p>
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"
596 + "<D:prop>\n"
597 + "<D:getetag/>\n"
598 + "<D:getcontenttype/>\n"
599 + "<D:getlastmodified/>\n"
600 + "<" + dataType + "-data/>\n"
601 + "</D:prop>\n"
602 + "%s"
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));
611 int limit;
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()) );
627 if (root == null) {
628 Log.w(TAG, "Unable to sync collection " + this.collectionPath + " (ID:" + this.collectionId
629 + " - no data from server).");
630 return;
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;
646 if ( cv == null ) {
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();
665 try {
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 ) {
672 if ( mi != null ) {
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;
687 return;
692 for( String href : toBeRemoved ) {
693 Log.v(TAG,"Did not find +"+href+"+ in the list.");
695 if ( toBeRemoved.size() > 0 ) {
696 doRegularSyncPropfind();
697 return;
700 return;
705 * <p>
706 * Finds the resources which have been marked as needing synchronisation in our local database.
707 * </p>
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),
720 null,
721 DavResources.NEEDS_SYNC + " = 1 OR "+DavResources.RESOURCE_DATA+" IS NULL",
722 null, null);
723 ContentQueryMap cqm = new ContentQueryMap(mCursor, DavResources.RESOURCE_NAME, false, null);
724 cqm.requery();
725 originalData = cqm.getRows();
726 mCursor.close();
727 if (Constants.LOG_VERBOSE) Log.v(TAG, "DavCollections ContentQueryMap retrieved in " + (System.currentTimeMillis() - start) + "ms");
728 return originalData;
732 * <p>
733 * Parse a single &lt;response&gt; node within a &lt;multistatus&gt;
734 * </p>
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;
740 DavNode prop = null;
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);
745 break;
749 if ( prop == null ) {
750 validResourceResponse = false;
752 else {
753 String s = prop.getFirstNodeText("getctag");
754 if ( s != null ) {
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);
761 else {
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);
769 return false;
771 else {
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:");
782 // Log.v(TAG,data);
783 // Log.v(TAG,StaticHelpers.toHexString(data.substring(0,40).getBytes()));
784 // }
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;
805 * <p>
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.
808 * </p>
810 * @param originalData
811 * <p>
812 * The href => Contentvalues map.
813 * </p>
814 * @param hrefs
815 * <p>
816 * The array of hrefs
817 * </p>
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);
824 String path;
825 InputStream in;
826 int status;
828 for (int hrefIndex = 0; hrefIndex < hrefs.length; hrefIndex++) {
829 path = collectionPath + hrefs[hrefIndex];
831 try {
832 in = requestor.doRequest("GET", path, headers, "");
834 catch (ConnectionFailedException e) {
835 Log.i(TAG,"ConnectionFailedException ("+e.getMessage()+") on GET from "+path);
836 continue;
838 catch (SendRequestFailedException e) {
839 Log.i(TAG,"SendRequestFailedException ("+e.getMessage()+") on GET from "+path);
840 continue;
842 catch (SSLException e) {
843 Log.i(TAG,"SSLException on GET from "+path);
844 continue;
846 if (in == null) {
847 if (Constants.LOG_DEBUG) Log.d(TAG, "Error - Unable to get data stream from server.");
848 continue;
850 else {
851 status = requestor.getStatusCode();
852 switch (status) {
853 case 200: // Status O.K.
854 StringBuilder resourceData = new StringBuilder();
855 BufferedReader r = new BufferedReader(new InputStreamReader(in));
856 String line;
857 try {
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());
871 break;
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] );
878 break;
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");
890 return;
895 private boolean timeToRun() {
896 if ( synchronisationForced ) {
897 if (Constants.LOG_VERBOSE) Log.v(TAG, "Synchronising now, since a sync has been forced.");
898 return true;
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.");
903 return 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.");
908 return true;
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);
922 else
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);
971 return result;
974 @Override
975 public String getDescription() {
976 return "Syncing collection contents of collection " + collectionId;