org.anhonesteffort.flock.MigrationService.java Source code

Java tutorial

Introduction

Here is the source code for org.anhonesteffort.flock.MigrationService.java

Source

/*
 * *
 *  Copyright (C) 2014 Open Whisper Systems
 *
 *  This program 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.
 *
 *  This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 * /
 */

package org.anhonesteffort.flock;

import android.app.NotificationManager;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.common.base.Optional;

import org.anhonesteffort.flock.auth.DavAccount;
import org.anhonesteffort.flock.crypto.KeyHelper;
import org.anhonesteffort.flock.crypto.KeyStore;
import org.anhonesteffort.flock.crypto.MasterCipher;
import org.anhonesteffort.flock.registration.RegistrationApi;
import org.anhonesteffort.flock.registration.RegistrationApiException;
import org.anhonesteffort.flock.sync.AbstractLocalComponentCollection;
import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler;
import org.anhonesteffort.flock.sync.addressbook.HidingCardDavStore;
import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore;
import org.anhonesteffort.flock.sync.addressbook.LocalContactCollection;
import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler;
import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore;
import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore;
import org.anhonesteffort.flock.sync.calendar.LocalEventCollection;
import org.anhonesteffort.flock.sync.key.DavKeyCollection;
import org.anhonesteffort.flock.sync.key.DavKeyStore;
import org.anhonesteffort.flock.webdav.InvalidComponentException;
import org.anhonesteffort.flock.webdav.PropertyParseException;
import org.anhonesteffort.flock.webdav.caldav.CalDavStore;
import org.anhonesteffort.flock.webdav.carddav.CardDavStore;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavServletResponse;

import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * rhodey
 */
public class MigrationService extends Service {

    public static final String ACTION_MIGRATION_STARTED = "org.anhonesteffort.flock.MigrationService.ACTION_MIGRATION_STARTED";
    public static final String ACTION_MIGRATION_COMPLETE = "org.anhonesteffort.flock.MigrationService.ACTION_MIGRATION_COMPLETE";

    private static final String TAG = MigrationService.class.getSimpleName();
    private static final String PREFERENCES_NAME = "MigrationService.PREFERENCES_NAME";

    private static final String KEY_STATE = "MigrationService.KEY_STATE";
    private static final String KEY_TIME_FIRST_CALENDAR_SYNC = "MigrationService.KEY_TIME_FIRST_CALENDAR_SYNC";
    private static final String KEY_TIME_FIRST_ADDRESSBOOK_SYNC = "MigrationService.KEY_TIME_FIRST_ADDRESSBOOK_SYNC";

    private static final int STATE_STARTED_MIGRATION = 1;
    private static final int STATE_SYNCED_WITH_REMOTE = 2;
    private static final int STATE_DELETED_KEY_COLLECTION = 3;
    private static final int STATE_GENERATED_NEW_KEYS = 4;
    private static final int STATE_REPLACED_KEY_COLLECTION = 5;
    private static final int STATE_REPLACED_KEYS = 6;
    private static final int STATE_DELETED_REMOTE_CALENDARS_AND_ADDRESSBOOKS = 7;
    private static final int STATE_REPLACED_REMOTE_CALENDARS = 8;
    private static final int STATE_REPLACED_REMOTE_ADDRESSBOOKS = 9;
    private static final int STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS = 10;
    private static final int STATE_REPLACED_EVENTS_AND_CONTACTS = 11;
    private static final int STATE_MIGRATION_COMPLETE = 12;

    private static final long KICK_INTERVAL_MILLISECONDS = 5000;

    private final Handler messageHandler = new Handler();
    private ServiceHandler serviceHandler;
    private Timer intervalTimer;
    private NotificationManager notifyManager;
    private NotificationCompat.Builder notificationBuilder;

    private DavAccount account;
    private MasterCipher masterCipher;

    private void setState(int state) {
        Log.d(TAG, "setState() >> " + state);

        SharedPreferences settings = getBaseContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);

        settings.edit().putInt(KEY_STATE, state).apply();
        handleUpdateNotificationUsingState();
    }

    private static int getState(Context context) {
        SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);

        Log.d(TAG, "getState() >> " + settings.getInt(KEY_STATE, 0));

        return settings.getInt(KEY_STATE, 0);
    }

    public static void hackOnActionPushCreatedContacts(Context context) {
        if (getState(context) == STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS) {
            Log.d(TAG, "just finished pushing new contacts during replace events and contacts state"
                    + ", marking early finish of contact sync.");
            new AddressbookSyncScheduler(context).setTimeLastSync(new Date().getTime());
        }
    }

    private void recordTimeFirstSync(String key, long timeMilliseconds) {
        Log.d(TAG, "recordTimeFirstSync() >> " + key + " " + timeMilliseconds);

        SharedPreferences settings = getBaseContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);

        settings.edit().putLong(key, timeMilliseconds).apply();
    }

    private long getTimeFirstSync(String key) {
        SharedPreferences settings = getBaseContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);

        return settings.getLong(key, -1);
    }

    private void handleInitializeNotification() {
        Log.d(TAG, "handleInitializeNotification()");

        notificationBuilder.setContentTitle(getString(R.string.migrating_to_new_version))
                .setContentText(getString(R.string.preparing_to_upgrade_sync_protocol)).setProgress(0, 0, true)
                .setSmallIcon(R.drawable.flock_actionbar_icon);

        startForeground(1030, notificationBuilder.build());
    }

    private void handleAskUserToEnableSync() {
        Log.d(TAG, "handleAskUserToEnableSync()");

        notificationBuilder.setContentTitle(getString(R.string.please_enable_sync))
                .setContentText(getString(R.string.please_enable_anrdoid_sync_to_complete_migration))
                .setProgress(0, 0, false).setSmallIcon(R.drawable.alert_warning_light);

        startForeground(1030, notificationBuilder.build());
    }

    private void handleUpdateNotificationUsingState() {
        Log.d(TAG, "handleUpdateNotificationUsingState() >> " + getState(getBaseContext()));

        notificationBuilder.setContentTitle(getString(R.string.migrating_to_new_version))
                .setSmallIcon(R.drawable.flock_actionbar_icon).setProgress(0, 0, true);

        switch (getState(getBaseContext())) {
        case STATE_STARTED_MIGRATION:
            notificationBuilder.setContentText(getString(R.string.checking_sync_for_new_contacts_and_calendar));
            break;

        case STATE_SYNCED_WITH_REMOTE:
        case STATE_DELETED_KEY_COLLECTION:
        case STATE_GENERATED_NEW_KEYS:
        case STATE_REPLACED_KEY_COLLECTION:
            notificationBuilder.setContentText(getString(R.string.generating_new_encryption_secrets));
            break;

        case STATE_REPLACED_KEYS:
        case STATE_DELETED_REMOTE_CALENDARS_AND_ADDRESSBOOKS:
        case STATE_REPLACED_REMOTE_CALENDARS:
            notificationBuilder.setContentText(getString(R.string.replacing_addressbooks_and_calendars));
            break;

        case STATE_REPLACED_REMOTE_ADDRESSBOOKS:
        case STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS:
            notificationBuilder.setContentText(getString(R.string.replacing_old_contacts_and_events));
            break;

        case STATE_REPLACED_EVENTS_AND_CONTACTS:
            notificationBuilder.setContentText(getString(R.string.finalizing_migration));
            break;

        case STATE_MIGRATION_COMPLETE:
            notificationBuilder.setContentTitle(getString(R.string.migration_complete));
            notificationBuilder.setContentText(getString(R.string.please_update_flock_on_all_your_devices));
            break;
        }

        notifyManager.notify(1030, notificationBuilder.build());
    }

    private void handleMigrationComplete() {
        Log.d(TAG, "handleMigrationComplete()");

        if (intervalTimer != null)
            intervalTimer.cancel();

        Intent intent = new Intent();
        intent.setPackage(MigrationHelperBroadcastReceiver.class.getPackage().getName());
        intent.setAction(ACTION_MIGRATION_COMPLETE);
        sendBroadcast(intent);

        stopForeground(false);
        stopSelf();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy()");

        if (getState(getBaseContext()) == STATE_MIGRATION_COMPLETE) {
            notificationBuilder.setSmallIcon(R.drawable.flock_actionbar_icon)
                    .setContentTitle(getString(R.string.migration_complete))
                    .setContentText(getString(R.string.please_update_flock_on_all_your_devices))
                    .setProgress(0, 0, false);

            notifyManager.notify(1030, notificationBuilder.build());
        }
    }

    private void handleEnableAllSyncAdapters() {
        new CalendarsSyncScheduler(getBaseContext()).setSyncEnabled(account.getOsAccount(), true);
        new AddressbookSyncScheduler(getBaseContext()).setSyncEnabled(account.getOsAccount(), true);
    }

    private void handleDisableAllSyncAdapters() {
        new CalendarsSyncScheduler(getBaseContext()).setSyncEnabled(account.getOsAccount(), false);
        new AddressbookSyncScheduler(getBaseContext()).setSyncEnabled(account.getOsAccount(), false);

        new CalendarsSyncScheduler(getBaseContext()).cancelPendingSyncs(account.getOsAccount());
        new AddressbookSyncScheduler(getBaseContext()).cancelPendingSyncs(account.getOsAccount());
    }

    private void handleException(Exception e) {
        if (e instanceof IOException) {
            IOException ex = (IOException) e;

            if (ex instanceof SocketException || ex instanceof UnknownHostException
                    || ex instanceof SocketTimeoutException) {
                Log.d(TAG, "experienced connection error during migration, will continue trying.", e);
            } else
                Log.d(TAG, "caught unknown IOException during migration >> " + e.toString(), e);
        } else
            Log.d(TAG, "caught exception during migration >> " + e.toString(), e);
    }

    private void handleStartMigration() {
        Log.d(TAG, "handleStartMigration()");

        handleInitializeNotification();
        handleStartKickingMigration();

        KeyStore.setUseCipherVersionZero(getBaseContext(), true);

        if (DavAccountHelper.isUsingOurServers(getBaseContext())) {
            try {

                new RegistrationApi(getBaseContext()).setAccountVersion(account, 2);

            } catch (RegistrationApiException e) {
                handleException(e);
                return;
            } catch (IOException e) {
                handleException(e);
                return;
            }
        }

        Intent intent = new Intent();
        intent.setPackage(MigrationHelperBroadcastReceiver.class.getPackage().getName());
        intent.setAction(ACTION_MIGRATION_STARTED);
        sendBroadcast(intent);

        setState(STATE_STARTED_MIGRATION);
    }

    private void handleSyncWithRemote() {
        Log.d(TAG, "handleSyncWithRemote()");

        handleEnableAllSyncAdapters();
        if (!ContentResolver.getMasterSyncAutomatically()) {
            handleAskUserToEnableSync();
            return;
        }
        handleUpdateNotificationUsingState();

        Long firstRecordedCalendarSync = getTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC);
        Optional<Long> timeLastCalendarSync = new CalendarsSyncScheduler(getBaseContext()).getTimeLastSync();

        if (firstRecordedCalendarSync <= 0) {
            if (!timeLastCalendarSync.isPresent())
                recordTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC, new Date().getTime());
            else
                recordTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC, timeLastCalendarSync.get());

            new CalendarsSyncScheduler(getBaseContext()).requestSync();
        }

        Long firstRecordedAddressbookSync = getTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC);
        Optional<Long> timeLastAddressbookSync = new AddressbookSyncScheduler(getBaseContext()).getTimeLastSync();

        if (firstRecordedAddressbookSync <= 0) {
            if (!timeLastAddressbookSync.isPresent())
                recordTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC, new Date().getTime());
            else
                recordTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC, timeLastAddressbookSync.get());

            new AddressbookSyncScheduler(getBaseContext()).requestSync();
        }

        if (firstRecordedCalendarSync > 0 && firstRecordedAddressbookSync > 0) {
            if (timeLastCalendarSync.isPresent() && timeLastAddressbookSync.isPresent()) {
                if (timeLastCalendarSync.get() > firstRecordedCalendarSync
                        && timeLastAddressbookSync.get() > firstRecordedAddressbookSync) {
                    if (new CalendarsSyncScheduler(getBaseContext()).syncInProgress(account.getOsAccount())
                            || new AddressbookSyncScheduler(getBaseContext())
                                    .syncInProgress(account.getOsAccount())) {
                        Log.w(TAG, "finished syncing with remote but waiting for active syncs to complete.");
                        handleDisableAllSyncAdapters();
                        return;
                    }

                    KeyStore.setUseCipherVersionZero(getBaseContext(), false);
                    handleDisableAllSyncAdapters();

                    recordTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC, -1);
                    recordTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC, -1);

                    setState(STATE_SYNCED_WITH_REMOTE);
                }
            }
        }
    }

    private void handleDeleteKeyCollection() {
        Log.d(TAG, "handleDeleteKeyCollection()");

        try {

            DavKeyStore keyStore = DavAccountHelper.getDavKeyStore(getBaseContext(), account);

            try {

                Optional<String> calendarHomeSet = keyStore.getCalendarHomeSet();
                keyStore.removeCollection(calendarHomeSet.get().concat(DavKeyStore.PATH_KEY_COLLECTION));

                setState(STATE_DELETED_KEY_COLLECTION);

            } catch (PropertyParseException e) {
                handleException(e);
            } catch (DavException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                keyStore.closeHttpConnection();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleGenerateNewKeys() {
        Log.d(TAG, "handleGenerateNewKeys()");

        try {

            KeyHelper.generateAndSaveSaltAndKeyMaterial(getBaseContext());
            masterCipher = KeyHelper.getMasterCipher(getBaseContext()).get();

            setState(STATE_GENERATED_NEW_KEYS);

        } catch (GeneralSecurityException e) {
            handleException(e);
        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleCreateKeyCollection() {
        Log.d(TAG, "handleCreateKeyCollection()");

        try {

            DavKeyStore.createCollection(getBaseContext(), account);

            setState(STATE_REPLACED_KEY_COLLECTION);

        } catch (PropertyParseException e) {
            handleException(e);
        } catch (DavException e) {

            if (e.getErrorCode() == DavServletResponse.SC_FORBIDDEN) {
                Log.w(TAG, "caught 403 when trying to create key collection, assuming already exists.");
                setState(STATE_REPLACED_KEY_COLLECTION);
            } else
                handleException(e);

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleReplaceKeys() {
        Log.d(TAG, "handleReplaceKeys()");

        try {

            DavKeyStore keyStore = DavAccountHelper.getDavKeyStore(getBaseContext(), account);

            try {

                Optional<DavKeyCollection> keyCollection = keyStore.getCollection();

                if (!keyCollection.isPresent()) {
                    Log.e(TAG, "missing key collection, reverting state to regenerate keys!");
                    setState(STATE_SYNCED_WITH_REMOTE);
                    return;
                }

                keyCollection.get().setMigrationStarted(getBaseContext());

                Optional<String> localKeyMaterialSalt = KeyHelper.buildEncodedSalt(getBaseContext());
                Optional<String> localEncryptedKeyMaterial = KeyStore.getEncryptedKeyMaterial(getBaseContext());

                if (localKeyMaterialSalt.isPresent() && localEncryptedKeyMaterial.isPresent()) {
                    keyCollection.get().setKeyMaterialSalt(localKeyMaterialSalt.get());
                    keyCollection.get().setEncryptedKeyMaterial(localEncryptedKeyMaterial.get());

                    setState(STATE_REPLACED_KEYS);
                } else {
                    Log.e(TAG, "missing key material, reverting state to regenerate keys!");
                    setState(STATE_SYNCED_WITH_REMOTE);
                }

            } catch (InvalidComponentException e) {
                handleException(e);
            } catch (PropertyParseException e) {
                handleException(e);
            } catch (DavException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                keyStore.closeHttpConnection();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleDeleteRemoteCalendarsAndAddressbooks() {
        Log.d(TAG, "handleDeleteRemoteCalendarsAndAddressbooks()");

        try {

            CalDavStore remoteCalendarStore = DavAccountHelper.getCalDavStore(getBaseContext(), account);
            CardDavStore remoteAddressbookStore = DavAccountHelper.getCardDavStore(getBaseContext(), account);

            try {

                LocalCalendarStore localCalendarStore = new LocalCalendarStore(getBaseContext(),
                        account.getOsAccount());
                LocalAddressbookStore localAddressbookStore = new LocalAddressbookStore(getBaseContext(), account);

                for (LocalEventCollection localCollection : localCalendarStore.getCollections()) {
                    if (remoteCalendarStore.getCollection(localCollection.getPath()).isPresent()) {
                        Log.d(TAG, "deleting remote caldav collection at >> " + localCollection.getPath());
                        remoteCalendarStore.removeCollection(localCollection.getPath());
                    }
                }

                for (LocalContactCollection localCollection : localAddressbookStore.getCollections()) {
                    if (remoteAddressbookStore.getCollection(localCollection.getPath()).isPresent()) {
                        Log.d(TAG, "deleting remote carddav collection at >> " + localCollection.getPath());
                        remoteAddressbookStore.removeCollection(localCollection.getPath());
                    }
                }

                setState(STATE_DELETED_REMOTE_CALENDARS_AND_ADDRESSBOOKS);

            } catch (RemoteException e) {
                handleException(e);
            } catch (DavException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                remoteCalendarStore.closeHttpConnection();
                remoteAddressbookStore.closeHttpConnection();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleReplaceRemoteCalendars() {
        Log.d(TAG, "handleReplaceRemoteCalendars()");

        try {

            HidingCalDavStore remoteCalendarStore = DavAccountHelper.getHidingCalDavStore(getBaseContext(), account,
                    masterCipher);

            try {

                LocalCalendarStore localCalendarStore = new LocalCalendarStore(getBaseContext(),
                        account.getOsAccount());

                for (LocalEventCollection localCollection : localCalendarStore.getCollections()) {
                    if (!remoteCalendarStore.getCollection(localCollection.getPath()).isPresent()) {
                        Log.d(TAG, "creating remote caldav collection at >> " + localCollection.getPath());

                        Optional<String> displayName = localCollection.getDisplayName();
                        Optional<Integer> color = localCollection.getColor();

                        if (displayName.isPresent() && color.isPresent())
                            remoteCalendarStore.addCollection(localCollection.getPath(), displayName.get(),
                                    color.get());
                        else if (displayName.isPresent()) {
                            remoteCalendarStore.addCollection(localCollection.getPath(), displayName.get(),
                                    getBaseContext().getResources().getColor(R.color.flocktheme_color));
                        } else
                            remoteCalendarStore.addCollection(localCollection.getPath());
                    }
                }

                setState(STATE_REPLACED_REMOTE_CALENDARS);

            } catch (RemoteException e) {
                handleException(e);
            } catch (DavException e) {

                if (e.getErrorCode() != DavServletResponse.SC_FORBIDDEN)
                    handleException(e);

            } catch (GeneralSecurityException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                remoteCalendarStore.releaseConnections();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleReplaceRemoteAddressbooks() {
        Log.d(TAG, "handleReplaceRemoteAddressbooks()");

        try {

            HidingCardDavStore remoteAddressbookStore = DavAccountHelper.getHidingCardDavStore(getBaseContext(),
                    account, masterCipher);

            try {

                LocalAddressbookStore localAddressbookStore = new LocalAddressbookStore(getBaseContext(), account);

                for (LocalContactCollection localCollection : localAddressbookStore.getCollections()) {
                    if (!remoteAddressbookStore.getCollection(localCollection.getPath()).isPresent()) {
                        Log.d(TAG, "creating remote carddav collection at >> " + localCollection.getPath());

                        Optional<String> displayName = localCollection.getDisplayName();
                        if (displayName.isPresent())
                            remoteAddressbookStore.addCollection(localCollection.getPath(), displayName.get());
                        else
                            remoteAddressbookStore.addCollection(localCollection.getPath());
                    }
                }

                setState(STATE_REPLACED_REMOTE_ADDRESSBOOKS);

            } catch (DavException e) {

                if (e.getErrorCode() != DavServletResponse.SC_FORBIDDEN)
                    handleException(e);

            } catch (GeneralSecurityException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                remoteAddressbookStore.releaseConnections();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleMarkAllComponentsAsNew(AbstractLocalComponentCollection<?> localCollection)
            throws RemoteException, OperationApplicationException {
        Log.d(TAG, "handleMarkAllComponentsAsNew() >> " + localCollection.getPath());

        for (Long componentId : localCollection.getComponentIds()) {
            localCollection.queueForMigration(componentId);
            localCollection.commitPendingOperations();
        }
    }

    private void handleMarkAllLocalEventsAndContactsAsNew() {
        Log.d(TAG, "handleMarkAllLocalEventsAndContactsAsNew()");

        LocalCalendarStore localCalendarStore = new LocalCalendarStore(getBaseContext(), account.getOsAccount());
        LocalAddressbookStore localAddressbookStore = new LocalAddressbookStore(getBaseContext(), account);

        try {

            for (LocalEventCollection collection : localCalendarStore.getCollections()) {
                Log.d(TAG, "marking all components as new in collection >> " + collection.getPath());
                handleMarkAllComponentsAsNew(collection);
            }

            for (LocalContactCollection collection : localAddressbookStore.getCollections()) {
                Log.d(TAG, "marking all components as new in collection >> " + collection.getPath());
                handleMarkAllComponentsAsNew(collection);
            }

            setState(STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS);

        } catch (OperationApplicationException e) {
            handleException(e);
        } catch (RemoteException e) {
            handleException(e);
        }
    }

    private void handleReplaceEventsAndContacts() {
        Log.d(TAG, "handleReplaceEventsAndContacts()");

        handleEnableAllSyncAdapters();
        if (!ContentResolver.getMasterSyncAutomatically()) {
            handleAskUserToEnableSync();
            return;
        }
        handleUpdateNotificationUsingState();

        Long firstRecordedCalendarSync = getTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC);
        Optional<Long> timeLastCalendarSync = new CalendarsSyncScheduler(getBaseContext()).getTimeLastSync();

        if (firstRecordedCalendarSync <= 0) {
            if (!timeLastCalendarSync.isPresent())
                recordTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC, new Date().getTime());
            else
                recordTimeFirstSync(KEY_TIME_FIRST_CALENDAR_SYNC, timeLastCalendarSync.get());

            new CalendarsSyncScheduler(getBaseContext()).requestSync();
        }

        Long firstRecordedAddressbookSync = getTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC);
        Optional<Long> timeLastAddressbookSync = new AddressbookSyncScheduler(getBaseContext()).getTimeLastSync();

        if (firstRecordedAddressbookSync <= 0) {
            if (!timeLastAddressbookSync.isPresent())
                recordTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC, new Date().getTime());
            else
                recordTimeFirstSync(KEY_TIME_FIRST_ADDRESSBOOK_SYNC, timeLastAddressbookSync.get());

            new AddressbookSyncScheduler(getBaseContext()).requestSync();
        }

        if (firstRecordedCalendarSync > 0 && firstRecordedAddressbookSync > 0) {
            if (timeLastCalendarSync.isPresent() && timeLastAddressbookSync.isPresent()) {
                if (timeLastCalendarSync.get() > firstRecordedCalendarSync
                        && timeLastAddressbookSync.get() > firstRecordedAddressbookSync) {
                    setState(STATE_REPLACED_EVENTS_AND_CONTACTS);
                }
            }
        }
    }

    private void handleSetMigrationComplete() {
        Log.d(TAG, "handleSetMigrationComplete()");

        try {

            DavKeyStore davKeyStore = DavAccountHelper.getDavKeyStore(getBaseContext(), account);

            try {

                Optional<DavKeyCollection> keyCollection = davKeyStore.getCollection();

                if (keyCollection.isPresent()) {
                    keyCollection.get().setMigrationComplete(getBaseContext());
                    setState(STATE_MIGRATION_COMPLETE);
                } else {
                    Log.e(TAG, "missing key collection, reverting state to regenerate keys!");
                    setState(STATE_SYNCED_WITH_REMOTE);
                }

            } catch (InvalidComponentException e) {
                handleException(e);
            } catch (PropertyParseException e) {
                handleException(e);
            } catch (DavException e) {
                handleException(e);
            } catch (IOException e) {
                handleException(e);
            } finally {
                davKeyStore.closeHttpConnection();
            }

        } catch (IOException e) {
            handleException(e);
        }
    }

    private void handleStartOrResumeMigration() {
        Log.d(TAG, "handleStartOrResumeMigration()");

        switch (getState(getBaseContext())) {
        case STATE_STARTED_MIGRATION:
            handleSyncWithRemote();
            if (getState(getBaseContext()) == STATE_STARTED_MIGRATION)
                break;

        case STATE_SYNCED_WITH_REMOTE:
            handleDeleteKeyCollection();
            if (getState(getBaseContext()) <= STATE_SYNCED_WITH_REMOTE)
                break;

        case STATE_DELETED_KEY_COLLECTION:
            handleGenerateNewKeys();
            if (getState(getBaseContext()) <= STATE_DELETED_KEY_COLLECTION)
                break;

        case STATE_GENERATED_NEW_KEYS:
            handleCreateKeyCollection();
            if (getState(getBaseContext()) <= STATE_GENERATED_NEW_KEYS)
                break;

        case STATE_REPLACED_KEY_COLLECTION:
            handleReplaceKeys();
            if (getState(getBaseContext()) <= STATE_REPLACED_KEY_COLLECTION)
                break;

        case STATE_REPLACED_KEYS:
            handleDeleteRemoteCalendarsAndAddressbooks();
            if (getState(getBaseContext()) <= STATE_REPLACED_KEYS)
                break;

        case STATE_DELETED_REMOTE_CALENDARS_AND_ADDRESSBOOKS:
            handleReplaceRemoteCalendars();
            if (getState(getBaseContext()) <= STATE_DELETED_REMOTE_CALENDARS_AND_ADDRESSBOOKS)
                break;

        case STATE_REPLACED_REMOTE_CALENDARS:
            handleReplaceRemoteAddressbooks();
            if (getState(getBaseContext()) <= STATE_REPLACED_REMOTE_CALENDARS)
                break;

        case STATE_REPLACED_REMOTE_ADDRESSBOOKS:
            handleMarkAllLocalEventsAndContactsAsNew();
            if (getState(getBaseContext()) <= STATE_REPLACED_REMOTE_ADDRESSBOOKS)
                break;

        case STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS:
            handleReplaceEventsAndContacts();
            if (getState(getBaseContext()) <= STATE_READY_TO_REPLACE_EVENTS_AND_CONTACTS)
                break;

        case STATE_REPLACED_EVENTS_AND_CONTACTS:
            handleSetMigrationComplete();
            if (getState(getBaseContext()) <= STATE_REPLACED_EVENTS_AND_CONTACTS)
                break;

        case STATE_MIGRATION_COMPLETE:
            handleMigrationComplete();
            break;

        default:
            handleStartMigration();
        }
    }

    @Override
    public void onCreate() {
        HandlerThread thread = new HandlerThread("MigrationService", HandlerThread.NORM_PRIORITY);
        thread.start();

        Looper serviceLooper = thread.getLooper();

        serviceHandler = new ServiceHandler(serviceLooper);
        notifyManager = (NotificationManager) getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE);
        notificationBuilder = new NotificationCompat.Builder(getBaseContext());

        try {

            Optional<DavAccount> account = DavAccountHelper.getAccount(getBaseContext());
            Optional<MasterCipher> masterCipher = KeyHelper.getMasterCipher(getBaseContext());

            if (account.isPresent() && masterCipher.isPresent()) {
                this.account = account.get();
                this.masterCipher = masterCipher.get();
            } else
                Log.e(TAG, "ACCOUNT NOT PRESENT xxx 0.O");

        } catch (IOException e) {
            Log.e(TAG, "exception while getting MasterCipher >> " + e);
        }
    }

    private final class ServiceHandler extends Handler {

        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "handleMessage()");

            if (account != null && masterCipher != null)
                handleStartOrResumeMigration();
            else
                Log.e(TAG, "missing account or master cipher! xxx");
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand()");

        serviceHandler.sendMessage(serviceHandler.obtainMessage());
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind()");
        return null;
    }

    private final Runnable kickMigrationRunnable = new Runnable() {
        @Override
        public void run() {
            getBaseContext().startService(new Intent(getBaseContext(), MigrationService.class));
        }
    };

    public void handleStartKickingMigration() {
        Log.d(TAG, "handleStartKickingMigration()");

        intervalTimer = new Timer();
        TimerTask kickMigrationTask = new TimerTask() {
            @Override
            public void run() {
                messageHandler.post(kickMigrationRunnable);
            }
        };

        intervalTimer.schedule(kickMigrationTask, 0, KICK_INTERVAL_MILLISECONDS);
    }
}