/*
* Copyright 2010 Arthur Zaczek <arthur@dasz.at>, dasz.at OG; All rights reserved.
* Copyright 2010 David Schmitt <david@dasz.at>, dasz.at OG; All rights reserved.
*
* This file is part of Kolab Sync for Android.
* Kolab Sync for Android is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
* Kolab Sync for Android is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Kolab Sync for Android.
* If not, see <http://www.gnu.org/licenses/>.
*/
package at.dasz.KolabDroid.Sync;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.mail.FetchProfile;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Flags.Flag;
import javax.xml.parsers.ParserConfigurationException;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import at.dasz.KolabDroid.R;
import at.dasz.KolabDroid.StatusHandler;
import at.dasz.KolabDroid.Calendar.SyncCalendarHandler;
import at.dasz.KolabDroid.ContactsContract.SyncContactsHandler;
import at.dasz.KolabDroid.Imap.ImapClient;
import at.dasz.KolabDroid.Provider.LocalCacheProvider;
import at.dasz.KolabDroid.Provider.StatusProvider;
import at.dasz.KolabDroid.Settings.Settings;
/**
* The background worker that implements the main synchronization algorithm.
*/
public class SyncWorker extends BaseWorker
{
// Not final to avoid warnings
private static boolean DBG_LOCAL_CHANGED = false;
private static boolean DBG_REMOTE_CHANGED = false;
public SyncWorker(Context context)
{
super(context);
}
private static StatusEntry status;
public static StatusEntry getStatus()
{
return status;
}
@Override
protected void runWorker()
{
setRunningMessage(R.string.syncisrunning);
StatusProvider statProvider = new StatusProvider(context);
try
{
StatusHandler.writeStatus(R.string.startsync);
Settings settings = new Settings(this.context);
SyncHandler handler = null;
handler = new SyncContactsHandler(this.context);
if (shouldProcess(handler))
{
status = handler.getStatus();
try
{
sync(settings, handler);
}
catch(Exception ex)
{
// Save fatal sync exception
status.setFatalErrorMsg(ex.toString());
throw ex;
}
finally
{
statProvider.saveStatusEntry(status);
}
}
if (isStopping())
{
StatusHandler.writeStatus(R.string.syncaborted);
return;
}
handler = new SyncCalendarHandler(this.context);
if (shouldProcess(handler))
{
status = handler.getStatus();
try
{
sync(settings, handler);
}
catch(Exception ex)
{
// Save fatal sync exception
status.setFatalErrorMsg(ex.toString());
throw ex;
}
finally
{
statProvider.saveStatusEntry(status);
}
}
if (isStopping())
{
StatusHandler.writeStatus(R.string.syncaborted);
return;
}
StatusHandler.writeStatus(R.string.syncfinished);
}
catch (Exception ex)
{
final String errorFormat = this.context.getResources().getString(
R.string.sync_error_format);
StatusHandler
.writeStatus(String.format(errorFormat, ex.toString()));
ex.printStackTrace();
}
finally
{
status = null;
statProvider.close();
StatusHandler.notifySyncFinished();
}
}
private boolean shouldProcess(SyncHandler handler)
{
return handler.shouldProcess();
/*
return handler.getDefaultFolderName() != null
&& !"".equals(handler.getDefaultFolderName());
*/
}
private void sync(Settings settings, SyncHandler handler)
throws MessagingException, IOException,
ParserConfigurationException, SyncException
{
//handler.setSettings(settings); //handler should be able to react on settings
StatusHandler.writeStatus(R.string.connect_server);
Store server = null;
Folder sourceFolder = null;
try
{
Session session = ImapClient.getDefaultImapSession(settings
.getPort(), settings.getUseSSL());
server = ImapClient.openServer(session, settings.getHost(),
settings.getUsername(), settings.getPassword());
StatusHandler.writeStatus(R.string.fetching_messages);
// Numbers in comments and messages reference Gargan's Algorithm and
// the wiki
// 1. retrieve list of all imap message headers
sourceFolder = server.getFolder(handler.getDefaultFolderName());
sourceFolder.open(Folder.READ_WRITE);
Message[] msgs = sourceFolder.getMessages();
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.CONTENT_INFO);
fp.add("Subject");
fp.add("Date");
sourceFolder.fetch(msgs, fp);
LocalCacheProvider cache = handler.getLocalCacheProvider();
Set<Integer> processedEntries = new HashSet<Integer>(
(int) (msgs.length * 1.2));
final String processMessageFormat = this.context.getResources()
.getString(R.string.processing_message_format);
final StatusEntry status = handler.getStatus();
for (Message m : msgs)
{
if (isStopping()) return;
if (m.getFlags().contains(Flag.DELETED))
{
Log.d("sync", "Found deleted message, continue");
continue;
}
SyncContext sync = new SyncContext();
try
{
sync.setMessage(m);
StatusHandler.writeStatus(String.format(
processMessageFormat, status.incrementItems(),
msgs.length));
// 2. check message headers for changes
String subject = sync.getMessage().getSubject();
Log.d("sync", "2. Checking message " + subject);
// 5. fetch local cache entry
sync.setCacheEntry(cache.getEntryFromRemoteId(subject));
if (sync.getCacheEntry() == null)
{
Log.i("sync", "6. found no local entry => save");
handler.createLocalItemFromServer(session, sourceFolder, sync);
status.incrementLocalNew();
if (sync.getCacheEntry() == null)
{
Log
.w(
"sync",
"createLocalItemFromServer returned a null object! See Logfile for parsing errors");
}
}
else
{
Log.d("sync", "7. compare data to figure out what happened");
boolean cacheIsSame = false;
if(settings.getCreateRemoteHash())
{
cacheIsSame = CacheEntry.isSameRemoteHash(sync.getCacheEntry(), sync.getMessage());
}
else
{
cacheIsSame = CacheEntry.isSame(sync.getCacheEntry(), sync.getMessage());
}
//if (CacheEntry.isSame(sync.getCacheEntry(), sync.getMessage()) && !DBG_REMOTE_CHANGED)
if(cacheIsSame && !DBG_REMOTE_CHANGED)
{
Log.d("sync", "7.a/d cur=localdb");
if (handler.hasLocalItem(sync))
{
Log
.d("sync",
"7.a check for local changes and upload them");
if (handler.hasLocalChanges(sync)
|| DBG_LOCAL_CHANGED)
{
Log
.i("sync",
"local changes found: updating ServerItem from Local");
handler.updateServerItemFromLocal(session,
sourceFolder, sync);
status.incrementRemoteChanged();
}
}
else
{
Log
.i("sync",
"7.d entry missing => delete on server");
handler.deleteServerItem(sync);
status.incrementRemoteDeleted();
}
}
else
{
Log
.d("sync",
"7.b/c check for local changes and \"resolve\" the conflict");
if (handler.hasLocalChanges(sync))
{
Log
.i("sync",
"7.c local changes found: conflicting, updating local item from server");
status.incrementConflicted();
}
else
{
Log.i("sync", "7.b no local changes found:"
+ " updating local item from server");
}
handler.updateLocalItemFromServer(sync);
status.incrementLocalChanged();
}
}
}
catch (SyncException ex)
{
Log.e("sync", ex.toString());
status.incrementErrors();
}
if (sync.getCacheEntry() != null)
{
Log.d("sync", "8. remember message as processed (item id="
+ sync.getCacheEntry().getLocalId() + ")");
processedEntries.add(sync.getCacheEntry().getLocalId());
}
}
// 9. for all unprocessed local items
// 9.a upload/delete
Log.d("sync", "9. process unprocessed local items");
Cursor c = handler.getAllLocalItemsCursor();
if (c == null) throw new SyncException("getAllLocalItems",
"cr.query returned null");
int currentLocalItemNo = 1;
try
{
final int idColIdx = handler.getIdColumnIndex(c);
final String processItemFormat = this.context.getResources()
.getString(R.string.processing_item_format);
while (c.moveToNext())
{
if (isStopping()) return;
int localId = c.getInt(idColIdx);
Log.d("sync", "9. processing #" + localId);
StatusHandler.writeStatus(String.format(processItemFormat,
currentLocalItemNo++, c.getCount()));
if (processedEntries.contains(localId))
{
// Log.d("sync",
// "9.a already processed from server: skipping");
continue;
}
SyncContext sync = new SyncContext();
sync.setCacheEntry(cache.getEntryFromLocalId(localId));
if (sync.getCacheEntry() != null)
{
Log.i("sync",
"9.b found in local cache: deleting locally");
handler.deleteLocalItem(sync);
status.incrementLocalDeleted();
status.incrementItems();
processedEntries.add(localId);
}
else
{
Log
.i("sync",
"9.c not found in local cache: creating on server");
handler.createServerItemFromLocal(session,
sourceFolder, sync, localId);
status.incrementRemoteNew();
status.incrementItems();
processedEntries.add(localId);
}
}
}
catch (SyncException ex)
{
Log.e("sync", ex.toString());
status.incrementErrors();
}
finally
{
if (!c.isClosed())
{
c.close();
}
}
}
finally
{
if (sourceFolder != null) sourceFolder.close(true);
if (server != null) server.close();
}
}
}