com.radicaldynamic.groupinform.services.DatabaseService.java Source code

Java tutorial

Introduction

Here is the source code for com.radicaldynamic.groupinform.services.DatabaseService.java

Source

/*
 * Copyright (C) 2009 University of Washington
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.radicaldynamic.groupinform.services;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.ektorp.CouchDbConnector;
import org.ektorp.CouchDbInstance;
import org.ektorp.DbAccessException;
import org.ektorp.ReplicationCommand;
import org.ektorp.ReplicationStatus;
import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.ektorp.impl.StdCouchDbConnector;
import org.ektorp.impl.StdCouchDbInstance;
import org.json.JSONObject;

import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

import com.radicaldynamic.gcmobile.android.preferences.PreferencesActivity;
import com.radicaldynamic.groupinform.R;
import com.radicaldynamic.groupinform.application.Collect;
import com.radicaldynamic.groupinform.database.InformCouchDbConnector;
import com.radicaldynamic.groupinform.documents.Generic;
import com.radicaldynamic.groupinform.logic.AccountFolder;
import com.radicaldynamic.groupinform.repositories.FormDefinitionRepo;
import com.radicaldynamic.groupinform.repositories.FormInstanceRepo;

/**
 * Database abstraction layer for CouchDB, based on Ektorp.
 * 
 * This does not control the stop/start of the actual DB.
 * See com.couchone.couchdb.CouchService for that.
 */
public class DatabaseService extends Service {
    private static final String t = "DatabaseService: ";

    // Replication modes
    public static final int REPLICATE_PUSH = 0;
    public static final int REPLICATE_PULL = 1;

    // Minutes represented as seconds 
    private static final int TIME_FIVE_MINUTES = 300;
    private static final int TIME_TEN_MINUTES = 600;

    // 24 hours (represented as milliseconds)
    private static final long TIME_24_HOURS = 86400000;

    // Values returned by the Couch service -- we can't connect to the localhost until these are known
    private String mLocalHost = null;
    private int mLocalPort = 0;

    private HttpClient mLocalHttpClient = null;
    private CouchDbInstance mLocalDbInstance = null;
    private CouchDbConnector mLocalDbConnector = null;

    private HttpClient mRemoteHttpClient = null;
    private CouchDbInstance mRemoteDbInstance = null;
    private CouchDbConnector mRemoteDbConnector = null;

    private boolean mInit = false;

    private boolean mConnectedToLocal = false;
    private boolean mConnectedToRemote = false;

    // This is the object that receives interactions from clients.  
    // See RemoteService for a more complete example.
    private final IBinder mBinder = new LocalBinder();

    private ConditionVariable mCondition;

    // Hash of database-to-last cleanup timestamp (used for controlled purging databases of placeholders)
    private Map<String, Long> mDbLastCleanup = new HashMap<String, Long>();

    // Hash of database-to-last replication timestamp
    private Map<String, Long> mDbLastReplication = new HashMap<String, Long>();

    private Runnable mTask = new Runnable() {
        final String tt = t + "mTask: ";

        public void run() {
            for (int i = 1; i > 0; ++i) {
                if (mInit == false) {
                    try {
                        mInit = true;

                        if (mLocalDbInstance != null) {
                            performHousekeeping();
                            synchronizeLocalDBs();
                        }
                    } catch (Exception e) {
                        if (Collect.Log.WARN)
                            Log.w(Collect.LOGTAG, tt + "error automatically connecting to DB: " + e.toString());
                    } finally {
                        mInit = false;
                    }
                }

                // Retry connection to CouchDB every 5 minutes
                if (mCondition.block(TIME_FIVE_MINUTES * 1000))
                    break;
            }
        }
    };

    @SuppressWarnings("serial")
    public class DbUnavailableException extends Exception {
        DbUnavailableException() {
            super();
        }
    }

    @SuppressWarnings("serial")
    public class DbUnavailableWhileOfflineException extends DbUnavailableException {
        DbUnavailableWhileOfflineException() {
            super();
        }
    }

    @SuppressWarnings("serial")
    public class DbUnavailableDueToMetadataException extends DbUnavailableException {
        DbUnavailableDueToMetadataException(String db) {
            super();
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, t + "metadata missing for DB " + db);
        }
    }

    @Override
    public void onCreate() {
        Thread persistentConnectionThread = new Thread(null, mTask, "DatabaseService");
        mCondition = new ConditionVariable(false);
        persistentConnectionThread.start();
    }

    @Override
    public void onDestroy() {
        mCondition.open();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, t + "received start ID " + startId + ": " + intent);
        return START_STICKY;
    }

    /**
     * Class for clients to access.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with
     * IPC.
     */
    public class LocalBinder extends Binder {
        public DatabaseService getService() {
            return DatabaseService.this;
        }
    }

    // Convenience method (uses currently selected database)
    public CouchDbConnector getDb() {
        return getDb(Collect.getInstance().getInformOnlineState().getSelectedDatabase());
    }

    public CouchDbConnector getDb(String db) {
        final String tt = t + "getDb(): ";

        AccountFolder folder = Collect.getInstance().getInformOnlineState().getAccountFolders().get(db);
        CouchDbConnector dbConnector;

        if (folder.isReplicated()) {
            // Local database
            try {
                open(db);
            } catch (DbUnavailableException e) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, tt + "unable to connect to local database server: " + e.toString());
            }

            dbConnector = mLocalDbConnector;
        } else {
            // Remote database
            try {
                open(db);
            } catch (DbUnavailableException e) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, tt + "unable to connect to remote database server: " + e.toString());
            }

            dbConnector = mRemoteDbConnector;
        }

        return dbConnector;
    }

    /*
     * Does a database exist on the local CouchDB instance?
     */
    public boolean isDbLocal(String db) {
        final String tt = t + "isDbLocal(): ";

        boolean result = false;

        try {
            if (mLocalDbInstance == null)
                connectToLocalServer();

            if (mLocalDbInstance.getAllDatabases().indexOf("db_" + db) != -1)
                result = true;
        } catch (DbAccessException e) {
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, tt + e.toString());
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "unhandled exception: " + e.toString());
        }

        return result;
    }

    /**
     * Open a specific database
     * 
     * @param database  Name of database to open
     * @return
     * @throws DbMetadataUnavailableException 
     * @throws DbUnavailableException 
     */
    public void open(String db) throws DbUnavailableException {
        // If database metadata is not yet available then abort here
        if (db == null || Collect.getInstance().getInformOnlineState().getAccountFolders().get(db) == null) {
            throw new DbUnavailableDueToMetadataException(db);
        }

        if (!Collect.getInstance().getIoService().isSignedIn()
                && !Collect.getInstance().getInformOnlineState().getAccountFolders().get(db).isReplicated())
            throw new DbUnavailableWhileOfflineException();

        boolean dbToOpenIsReplicated = Collect.getInstance().getInformOnlineState().getAccountFolders().get(db)
                .isReplicated();

        if (dbToOpenIsReplicated) {
            // Local database
            if (mConnectedToLocal) {
                if (mLocalDbConnector instanceof StdCouchDbConnector
                        && mLocalDbConnector.getDatabaseName().equals("db_" + db)) {
                    return;
                }
            } else {
                connectToLocalServer();
            }

            openLocalDb(db);
        } else {
            // Remote database
            if (mConnectedToRemote) {
                if (mRemoteDbConnector instanceof StdCouchDbConnector
                        && mRemoteDbConnector.getDatabaseName().equals("db_" + db)) {
                    return;
                }
            } else {
                connectToRemoteServer();
            }

            openRemoteDb(db);
        }
    }

    /*
     * Similar in purpose to performHousekeeping(), this method is targeted towards any database 
     */
    public void performHousekeeping(String db) throws DbUnavailableException {
        final String tt = t + "performHousekeeping(String): ";

        // Determine if this database needs to be cleaned up
        Long lastCleanup = mDbLastCleanup.get(db);

        if (lastCleanup == null || System.currentTimeMillis() / 1000 - lastCleanup >= TIME_TEN_MINUTES) {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "beginning cleanup for " + db);
            mDbLastCleanup.put(db, new Long(System.currentTimeMillis() / 1000));
            removePlaceholders(new FormDefinitionRepo(getDb()).getAllPlaceholders());
            removePlaceholders(new FormInstanceRepo(getDb()).getAllPlaceholders());
        }
    }

    synchronized public ReplicationStatus replicate(String db, int mode) {
        final String tt = t + "replicate(): ";

        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, tt + "about to replicate " + db);

        // Will not replicate unless signed in
        if (!Collect.getInstance().getIoService().isSignedIn()) {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "aborting replication: not signed in");
            return null;
        }

        if (Collect.getInstance().getInformOnlineState().isOfflineModeEnabled()) {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "aborting replication: offline mode is enabled");
            return null;
        }

        /*
         * Lookup master cluster by IP.  Do this instead of relying on Erlang's internal resolver 
         * (and thus Google's public DNS).  Our builds of Erlang for Android do not yet use 
         * Android's native DNS resolver.
         */
        String masterClusterIP = null;

        try {
            InetAddress[] clusterInetAddresses = InetAddress
                    .getAllByName(getString(R.string.tf_default_ionline_server));
            masterClusterIP = clusterInetAddresses[new Random().nextInt(clusterInetAddresses.length)]
                    .getHostAddress();
        } catch (UnknownHostException e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "unable to lookup master cluster IP addresses: " + e.toString());
            e.printStackTrace();
        }

        // Create local instance of database
        boolean dbCreated = false;

        // User may not have connected to local database yet - start up the connection for them
        try {
            if (mLocalDbInstance == null) {
                connectToLocalServer();
            }
        } catch (DbUnavailableException e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "cannot connect to local database server");
            e.printStackTrace();
        }

        if (mLocalDbInstance.getAllDatabases().indexOf("db_" + db) == -1) {
            switch (mode) {
            case REPLICATE_PULL:
                if (Collect.Log.INFO)
                    Log.i(Collect.LOGTAG, tt + "creating local database " + db);
                mLocalDbInstance.createDatabase("db_" + db);
                dbCreated = true;
                break;

            case REPLICATE_PUSH:
                // If the database does not exist client side then there is no point in continuing
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, tt + "cannot find local database " + db + " to push");
                return null;
            }
        }

        // Configure replication direction
        String source = null;
        String target = null;

        String deviceId = Collect.getInstance().getInformOnlineState().getDeviceId();
        String deviceKey = Collect.getInstance().getInformOnlineState().getDeviceKey();

        String localServer = "http://" + mLocalHost + ":" + mLocalPort + "/db_" + db;
        String remoteServer = "http://" + deviceId + ":" + deviceKey + "@" + masterClusterIP + ":5984/db_" + db;

        // Should we use encrypted transfers?
        SharedPreferences settings = PreferenceManager
                .getDefaultSharedPreferences(Collect.getInstance().getBaseContext());

        if (settings.getBoolean(PreferencesActivity.KEY_ENCRYPT_SYNCHRONIZATION, true)) {
            remoteServer = "https://" + deviceId + ":" + deviceKey + "@" + masterClusterIP + ":6984/db_" + db;
        }

        switch (mode) {
        case REPLICATE_PUSH:
            source = localServer;
            target = remoteServer;
            break;

        case REPLICATE_PULL:
            source = remoteServer;
            target = localServer;
            break;
        }

        ReplicationCommand cmd = new ReplicationCommand.Builder().source(source).target(target).build();
        ReplicationStatus status = null;

        try {
            status = mLocalDbInstance.replicate(cmd);
        } catch (Exception e) {
            // Remove a recently created DB if the replication failed
            if (dbCreated) {
                if (Collect.Log.ERROR)
                    Log.e(Collect.LOGTAG, t + "replication exception: " + e.toString());
                e.printStackTrace();

                mLocalDbInstance.deleteDatabase("db_" + db);
            }
        }

        return status;
    }

    public void setLocalDatabaseInfo(String host, int port) {
        final String tt = t + "setLocalDatabaseInfo(): ";

        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, tt + "set host and port to " + host + ":" + port);

        mLocalHost = host;
        mLocalPort = port;
    }

    synchronized private void connectToLocalServer() throws DbUnavailableException {
        final String tt = t + "connectToLocalServer(): ";

        if (mLocalHost == null || mLocalPort == 0) {
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, tt + "local host information not available; aborting connection");
            mConnectedToLocal = false;
            return;
        }

        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, tt + "establishing connection to " + mLocalHost + ":" + mLocalPort);

        try {
            /*
             * Socket timeout of 5 minutes is important, otherwise long-running replications
             * will fail.  It is possible that we will need to extend this in the future if
             * it turns out to be insufficient.
             */
            mLocalHttpClient = new StdHttpClient.Builder().host(mLocalHost).port(mLocalPort)
                    .socketTimeout(TIME_FIVE_MINUTES * 1000).build();

            mLocalDbInstance = new StdCouchDbInstance(mLocalHttpClient);
            mLocalDbInstance.getAllDatabases();

            if (mConnectedToLocal == false)
                mConnectedToLocal = true;

            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "connection to " + mLocalHost + " successful");
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + e.toString());
            e.printStackTrace();
            mConnectedToLocal = false;
            throw new DbUnavailableException();
        }
    }

    synchronized private void connectToRemoteServer() throws DbUnavailableException {
        final String tt = t + "connectToRemoteServer(): ";

        String host = getString(R.string.tf_default_ionline_server);
        int port = 6984;

        if (Collect.Log.DEBUG)
            Log.d(Collect.LOGTAG, tt + "establishing connection to " + host + ":" + port);

        try {
            mRemoteHttpClient = new StdHttpClient.Builder().enableSSL(true).host(host).port(port)
                    .socketTimeout(30 * 1000).username(Collect.getInstance().getInformOnlineState().getDeviceId())
                    .password(Collect.getInstance().getInformOnlineState().getDeviceKey()).build();

            mRemoteDbInstance = new StdCouchDbInstance(mRemoteHttpClient);
            mRemoteDbInstance.getAllDatabases();

            if (mConnectedToRemote == false)
                mConnectedToRemote = true;

            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "connection to " + host + " successful");
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "while connecting to server " + port + ": " + e.toString());
            e.printStackTrace();
            mConnectedToRemote = false;
            throw new DbUnavailableException();
        }
    }

    private void openLocalDb(String db) throws DbUnavailableException {
        final String tt = t + "openLocalDb(): ";

        try {
            /*
             * We used to create the database if it did not exist HOWEVER this had unintended side effects.
             * 
             * Since local databases are typically initialized on-demand the first time the user selects
             * them for operations, databases that were selected for replication but not yet "switched to" 
             * would be created as empty databases if the user backed out of the folder selection screen 
             * without specifically choosing a database.
             * 
             * Because the database then existed, attempts to "switch to" the database via the folder
             * selection screen (and have it initialized on-demand as expected) would fail.  At least,
             * until the system got around to creating and replicating it automatically.
             */
            if (mLocalDbInstance.getAllDatabases().indexOf("db_" + db) == -1) {
                if (Collect.Log.WARN)
                    Log.w(Collect.LOGTAG, tt + "database does not exist; failing attempt to open");
                throw new DbUnavailableException();
            }

            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "opening database " + db);
            mLocalDbConnector = new InformCouchDbConnector("db_" + db, mLocalDbInstance);
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "while opening DB " + db + ": " + e.toString());
            e.printStackTrace();
            throw new DbUnavailableException();
        }
    }

    private void openRemoteDb(String db) throws DbUnavailableException {
        final String tt = t + "openRemoteDb(): ";

        try {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "opening database " + db);
            mRemoteDbConnector = new InformCouchDbConnector("db_" + db, mRemoteDbInstance);

            /* 
             * This should trigger any 401:Unauthorized errors when connecting to a remote DB
             * (better to know about them now then to experience a crash later because we didn't trap something)
             */
            mRemoteDbConnector.getDbInfo();
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "while opening DB " + db + ": " + e.toString());
            e.printStackTrace();
            throw new DbUnavailableException();
        }
    }

    /*
     * Perform any house keeping (e.g., removing of unused DBs, view compaction & cleanup)
     */
    private void performHousekeeping() {
        final String tt = t + "performHousekeeping(): ";

        try {
            List<String> allDatabases = mLocalDbInstance.getAllDatabases();
            Iterator<String> dbs = allDatabases.iterator();

            while (dbs.hasNext()) {
                String db = dbs.next();

                // Skip special databases
                if (!db.startsWith("_")) {
                    // Our metadata knows nothing about the db_ prefix
                    db = db.substring(3);

                    AccountFolder folder = Collect.getInstance().getInformOnlineState().getAccountFolders().get(db);

                    if (folder == null) {
                        // Remove databases that exist locally but for which we have no metadata
                        if (Collect.Log.DEBUG)
                            Log.d(Collect.LOGTAG, tt + "no metatdata for " + db + " (removing)");
                        mLocalDbInstance.deleteDatabase("db_" + db);
                    } else if (isDbLocal(db) && folder.isReplicated() == false) {
                        // Purge any databases that are local but not on the replication list                        
                        try {
                            ReplicationStatus status = replicate(db, REPLICATE_PUSH);

                            if (status != null && status.isOk()) {
                                if (Collect.Log.DEBUG)
                                    Log.d(Collect.LOGTAG, tt + "final replication push successful, removing " + db);
                                mLocalDbInstance.deleteDatabase("db_" + db);
                            }
                        } catch (Exception e) {
                            if (Collect.Log.ERROR)
                                Log.e(Collect.LOGTAG,
                                        tt + "final replication push of " + db + " failed at " + e.toString());
                            e.printStackTrace();
                        }
                    }
                }
            }
        } catch (DbAccessException e) {
            if (Collect.Log.WARN)
                Log.w(Collect.LOGTAG, tt + "database not available " + e.toString());
        } catch (Exception e) {
            if (Collect.Log.ERROR)
                Log.e(Collect.LOGTAG, tt + "unhandled exception " + e.toString());
            e.printStackTrace();
        }
    }

    /*
     * Evaluate and remove a set of placeholders on the basis of who created it and when it was created
     */
    private void removePlaceholders(HashMap<String, JSONObject> placeholders) {
        final String tt = t + "removePlaceholders(): ";

        for (Map.Entry<String, JSONObject> entry : placeholders.entrySet()) {
            if (entry.getValue().optString("createdBy", null) == null
                    || entry.getValue().optString("dateCreated", null) == null) {
                // Remove old style (unowned) placeholders immediately
                try {
                    getDb().delete(entry.getKey(), entry.getValue().optString("_rev"));
                    if (Collect.Log.VERBOSE)
                        Log.v(Collect.LOGTAG, tt + "removed old-style placeholder " + entry.getKey());
                } catch (Exception e) {
                    if (Collect.Log.ERROR)
                        Log.e(Collect.LOGTAG, tt + "unable to remove old-style placeholder");
                    e.printStackTrace();
                }
            } else if (entry.getValue().optString("createdBy")
                    .equals(Collect.getInstance().getInformOnlineState().getDeviceId())) {
                // Remove placeholders owned by me immediately
                try {
                    getDb().delete(entry.getKey(), entry.getValue().optString("_rev"));
                    if (Collect.Log.VERBOSE)
                        Log.v(Collect.LOGTAG, tt + "removed my placeholder " + entry.getKey());
                } catch (Exception e) {
                    if (Collect.Log.ERROR)
                        Log.e(Collect.LOGTAG, tt + "unable to remove my placeholder");
                    e.printStackTrace();
                }
            } else {
                // Remove placeholders owned by other people if they are stale (older than a day)
                SimpleDateFormat sdf = new SimpleDateFormat(Generic.DATETIME);
                Calendar calendar = Calendar.getInstance();

                try {
                    calendar.setTime(sdf.parse(entry.getValue().optString("dateCreated")));

                    if (calendar.getTimeInMillis() - Calendar.getInstance().getTimeInMillis() > TIME_24_HOURS) {
                        try {
                            getDb().delete(entry.getKey(), entry.getValue().optString("_rev"));
                            if (Collect.Log.VERBOSE)
                                Log.v(Collect.LOGTAG, tt + "removed stale placeholder " + entry.getKey());
                        } catch (Exception e) {
                            if (Collect.Log.ERROR)
                                Log.e(Collect.LOGTAG, tt + "unable to remove stale placeholder");
                            e.printStackTrace();
                        }
                    }
                } catch (ParseException e1) {
                    if (Collect.Log.ERROR)
                        Log.e(Collect.LOGTAG, tt + "unable to parse dateCreated: " + e1.toString());
                    e1.printStackTrace();
                }
            }
        }
    }

    /*
     * Trigger a push/pull replication for each locally replicated database
     */
    private void synchronizeLocalDBs() {
        final String tt = t + "synchronizeLocalDBs(): ";

        // Do we use automatic synchronization?
        SharedPreferences settings = PreferenceManager
                .getDefaultSharedPreferences(Collect.getInstance().getBaseContext());

        // How often should we automatically synchronize databases?
        String syncInterval = settings.getString(PreferencesActivity.KEY_SYNCHRONIZATION_INTERVAL,
                Integer.toString(TIME_FIVE_MINUTES));

        if (settings.getBoolean(PreferencesActivity.KEY_AUTOMATIC_SYNCHRONIZATION, true)) {
            Set<String> folderSet = Collect.getInstance().getInformOnlineState().getAccountFolders().keySet();
            Iterator<String> folderIds = folderSet.iterator();

            while (folderIds.hasNext()) {
                AccountFolder folder = Collect.getInstance().getInformOnlineState().getAccountFolders()
                        .get(folderIds.next());

                if (folder.isReplicated()) {
                    // Determine if this database needs to be replicated
                    Long lastUpdate = mDbLastReplication.get(folder.getId());

                    if (lastUpdate == null
                            || System.currentTimeMillis() / 1000 - lastUpdate >= Integer.parseInt(syncInterval)) {
                        mDbLastReplication.put(folder.getId(), new Long(System.currentTimeMillis() / 1000));

                        if (Collect.Log.INFO)
                            Log.i(Collect.LOGTAG,
                                    tt + "about to begin automatic replication of " + folder.getName());

                        try {
                            replicate(folder.getId(), REPLICATE_PULL);
                            replicate(folder.getId(), REPLICATE_PUSH);
                        } catch (Exception e) {
                            if (Collect.Log.WARN)
                                Log.w(Collect.LOGTAG,
                                        tt + "problem replicating " + folder.getId() + ": " + e.toString());
                            e.printStackTrace();
                        }
                    } else {
                        if (Collect.Log.DEBUG)
                            Log.d(Collect.LOGTAG, tt + "skipping automatic replication of " + folder.getName()
                                    + ": last synchronization too recent");
                    }
                }
            }
        } else {
            if (Collect.Log.DEBUG)
                Log.d(Collect.LOGTAG, tt + "skipping (automatic synchronization disabled)");
        }
    }
}