com.ntsync.android.sync.syncadapter.SyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.ntsync.android.sync.syncadapter.SyncAdapter.java

Source

package com.ntsync.android.sync.syncadapter;

/*
 * Copyright (C) 2014 Markus Grieder
 * 
 * This file is based on SyncAdapter.java from the SampleSyncAdapter-Example in Android SDK
 *
 * 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/gpl-3.0.html>. 
 */

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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.
 */

import java.io.IOException;
import java.security.InvalidKeyException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.crypto.SecretKey;

import org.apache.http.ParseException;
import org.apache.http.auth.AuthenticationException;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.NetworkErrorException;
import android.accounts.OperationCanceledException;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
import android.util.Log;

import com.ntsync.android.sync.R;
import com.ntsync.android.sync.activities.KeyPasswordActivity;
import com.ntsync.android.sync.activities.PaymentVerificationService;
import com.ntsync.android.sync.activities.ShopActivity;
import com.ntsync.android.sync.activities.ViewAccountsActivity;
import com.ntsync.android.sync.client.ClientKeyHelper;
import com.ntsync.android.sync.client.ClientKeyHelper.PrivateKeyState;
import com.ntsync.android.sync.client.NetworkUtilities;
import com.ntsync.android.sync.client.ServerException;
import com.ntsync.android.sync.platform.ContactManager;
import com.ntsync.android.sync.platform.RestrictionConflictHandler;
import com.ntsync.android.sync.shared.AccountSyncResult;
import com.ntsync.android.sync.shared.Constants;
import com.ntsync.android.sync.shared.LogHelper;
import com.ntsync.android.sync.shared.SyncResultState;
import com.ntsync.android.sync.shared.SyncUtils;
import com.ntsync.shared.ContactConstants;
import com.ntsync.shared.ContactGroup;
import com.ntsync.shared.HeaderCreateException;
import com.ntsync.shared.HeaderParseException;
import com.ntsync.shared.RawContact;
import com.ntsync.shared.RequestGenerator.SyncResponse;
import com.ntsync.shared.Restrictions;
import com.ntsync.shared.SyncAnchor;

/**
 * implementation for syncing sample SyncAdapter contacts to the platform
 * ContactOperations provider. This sample shows a basic 2-way sync between the
 * client and a sample server. It also contains an example of how to update the
 * contacts' status messages, which would be useful for a messaging or social
 * networking client.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

    private static final String CLIENT_MOD_FAILED_MSG = "Update ClientMod failed";
    private static final Long FULLSYNC_MARKER = Long.valueOf(-1);
    private static final String TAG = "SyncAdapter";
    private static final String SYNC_CONTACT_MARKER_KEY = "com.ntsync.android.sync.contact.marker";
    private static final String SYNC_CONTACT_PHOTOSAVE_KEY = "com.ntsync.android.sync.contact.photosave";
    private static final String SYNC_GROUP_MARKER_KEY = "com.ntsync.android.sync.group.marker";

    private static final String NOTIF_SHOWN_PHOTO_SYNCED = "com.ntsync.android.sync.contact.notif.photo";
    private static final String NOTIF_SHOWN_CONTACTS_SYNCED = "com.ntsync.android.sync.contact.notif.contacts";

    /**
     * Time interval in Seconds for a retry when sync was failing due to an
     * IO-Error.
     */
    private static final int SYNC_RETRY_DELAY = 60;

    /** Time to wait until notification is shown again */
    private static final long NOTIF_WAIT_TIME = 1000 * 60 * 60 * 24 * 7;

    private final AccountManager mAccountManager;

    private final Context mContext;

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mContext = context;
        mAccountManager = AccountManager.get(context);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
            SyncResult syncResult) {

        PaymentVerificationService.startVerificationTimer(mContext.getApplicationContext());

        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext);
        boolean onlyWifi = settings.getBoolean("sync_onlywifi", false);
        if (onlyWifi) {
            WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
            if (!wifiManager.isWifiEnabled()) {
                Log.i(TAG, "Cancel Sync, because Wifi is not enabled.");
                syncResult.stats.numIoExceptions++;
                return;
            }
        }

        AccountSyncResult ourSyncResult = new AccountSyncResult(account.name);
        try {
            SyncUtils.stopProcessDataChanges();
            performSync(account, extras, syncResult, ourSyncResult);

            updateClientMod(account);
        } catch (InterruptedException e1) {
            LogHelper.logWCause(TAG, "Failed to sync.", e1);
            syncResult.databaseError = true;
        } finally {
            SyncUtils.startProcessDataChanges();
            ourSyncResult.setLastSyncTime(new Date());
            SyncUtils.setSyncResult(mAccountManager, account, ourSyncResult);
        }
    }

    private void performSync(Account account, Bundle extras, SyncResult syncResult,
            AccountSyncResult appSyncResult) {
        String authtoken = null;
        try {

            long contactSyncMarker = getContactSyncMarker(account);
            long contactGroupMarker = getContactGroupSyncMarker(account);
            SyncAnchor syncAnchor = new SyncAnchor();

            if (contactSyncMarker == 0) {
                // By default, contacts from a 3rd party provider are hidden
                // in the contacts list.
                ContactManager.setAccountContactsVisibility(mContext, account, true);

                // Add Default Group, otherwise group selection is not
                // visible
                // in the default editor.
                ContactManager.ensureSampleGroupExists(mContext, account);
            }

            authtoken = NetworkUtilities.blockingGetAuthToken(mAccountManager, account, null);
            if (authtoken == null) {
                syncResult.stats.numIoExceptions++;
                appSyncResult.setState(SyncResultState.AUTH_FAILED);
                return;
            }

            boolean getRestrictions = extras != null ? extras.containsKey(Constants.PARAM_GETRESTRICTIONS) : false;
            Restrictions restr = SyncUtils.getRestrictions(account, mAccountManager);
            if (restr == null || getRestrictions) {
                Restrictions oldRestr = restr;
                restr = NetworkUtilities.getRestrictions(mContext, account, authtoken, mAccountManager);
                processRestrictions(oldRestr, restr, account);
                // Return-Value is not processed because we don't have to
                // restart for a full sync because all relevant data values will
                // be read after here.
            }

            PrivateKeyState readyState = ClientKeyHelper.isReadyForSync(mContext, account, mAccountManager,
                    authtoken);
            boolean retryLater = false;
            switch (readyState) {
            case AUTH_FAILED:
                retryLater = true;
                appSyncResult.setState(SyncResultState.AUTH_FAILED);
                break;
            case CHECK_FAILED:
                // Retry later -> delay sync
                retryLater = true;
                appSyncResult.setState(SyncResultState.SERVER_ERROR);
                break;
            case NETWORK_ERROR:
                appSyncResult.setState(SyncResultState.NETWORK_ERROR);
                appSyncResult.setErrorMsg(readyState.getErrorMsg());
                retryLater = true;
                break;
            case MISSING_KEY:
                sendMissingKey(account, authtoken, readyState.getCurrSalt());
                appSyncResult.setState(SyncResultState.MISSING_KEY);
                return;
            default:
                clearMissingKeyNotification(account);
                break;
            }
            if (retryLater) {
                syncResult.delayUntil = SYNC_RETRY_DELAY;
                syncResult.fullSyncRequested = true;
                return;
            }

            SecretKey privKey = ClientKeyHelper.getOrCreatePrivateKey(account, mAccountManager);

            authtoken = checkIfSaltSaved(account, authtoken);
            if (authtoken == null) {
                syncResult.stats.numIoExceptions++;
                appSyncResult.setState(SyncResultState.AUTH_FAILED);
                return;
            }

            // Update ClientMod if not already set.
            ContactManager.updateClientModDate(mContext, account);
            // Get local Dirty Groups
            List<ContactGroup> dirtyGroups = ContactManager.getDirtyGroups(mContext, account, restr);
            boolean syncContacts = true;
            if (!dirtyGroups.isEmpty()) {
                // sync only Groups if some new groups are available, to
                // make
                // sure, that ids are available
                for (ContactGroup contactGroup : dirtyGroups) {
                    if (contactGroup.getSourceId() == null) {
                        syncContacts = false;
                        break;
                    }
                }
            }
            syncAnchor.setAnchor(ContactConstants.TYPE_CONTACTGROUP, contactGroupMarker);

            List<RawContact> dirtyContacts = null;
            Map<Long, String> newIdMap = null;
            if (syncContacts) {

                // Get local Dirty contacts
                dirtyContacts = ContactManager.getDirtyContacts(mContext, account, restr,
                        new SyncRestConflictHandler(account.name));

                newIdMap = ContactManager.getNewIdMap(mContext, account);
                syncAnchor.setAnchor(ContactConstants.TYPE_CONTACT, contactSyncMarker);
            }

            // Send the dirty contacts to the server, and retrieve the
            // server-side changes
            String saltStr = ClientKeyHelper.getSalt(account, mAccountManager);
            boolean explizitPhotoSave = getExplicitSavePhoto(account);
            SyncResponse result = NetworkUtilities.syncContacts(account, authtoken, syncAnchor, dirtyContacts,
                    dirtyGroups, privKey, mAccountManager, mContext, syncResult, saltStr, newIdMap, restr,
                    explizitPhotoSave);

            if (result.newGroupIdMap != null && !result.newGroupIdMap.isEmpty()) {
                if (Log.isLoggable(TAG, Log.INFO)) {
                    Log.i(TAG, "Calling contactManager's set new GroupIds. Count Updates:"
                            + result.newGroupIdMap.size());
                }

                ContactManager.saveGroupIds(mContext, account.name, result.newGroupIdMap);
            }
            if (result.newContactIdMap != null && !result.newContactIdMap.isEmpty()) {
                if (Log.isLoggable(TAG, Log.INFO)) {
                    Log.i(TAG, "Calling contactManager's set new ContactIds. Count Updates:"
                            + result.newContactIdMap.size());
                }

                ContactManager.saveContactIds(mContext, account.name, result.newContactIdMap);
            }

            Set<Long> updatedGroupIds = null;
            if (result.serverGroups != null && !result.serverGroups.isEmpty()) {
                // Update the local groups database with the changes.
                if (Log.isLoggable(TAG, Log.INFO)) {
                    Log.i(TAG,
                            "Calling contactManager's update groups. Count Updates:" + result.serverGroups.size());
                }
                updatedGroupIds = ContactManager.updateGroups(mContext, account.name, result.serverGroups);
            }
            Set<Long> updatedContactIds = null;
            if (result.serverContacts != null && !result.serverContacts.isEmpty()) {
                // Update the local contacts database with the changes.
                if (Log.isLoggable(TAG, Log.INFO)) {
                    Log.i(TAG, "Calling contactManager's update contacts. Count Updates:"
                            + result.serverContacts.size());
                }
                updatedContactIds = ContactManager.updateContacts(mContext, account.name, result.serverContacts,
                        true, restr);
            }

            SyncAnchor newSyncAnchor = result.newServerAnchor;
            if (newSyncAnchor != null) {
                setContactSyncMarker(account, newSyncAnchor.getAnchor(ContactConstants.TYPE_CONTACT));
                setContactGroupSyncMarker(account, newSyncAnchor.getAnchor(ContactConstants.TYPE_CONTACTGROUP));
            }

            if (result.syncstate != null) {
                switch (result.syncstate) {
                case INVALID_KEY:
                    // Reset Key-Info && do FullSync
                    mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authtoken);
                    ClientKeyHelper.clearPrivateKeyData(account, mAccountManager);
                    ContactManager.setDirtyFlag(mContext, account);
                    setContactSyncMarker(account, FULLSYNC_MARKER);
                    setContactGroupSyncMarker(account, FULLSYNC_MARKER);
                    syncResult.fullSyncRequested = true;
                    appSyncResult.setState(SyncResultState.SUCCESS);
                    return;
                case FORCE_FULLSYNC:
                    ContactManager.setDirtyFlag(mContext, account);
                    setContactSyncMarker(account, FULLSYNC_MARKER);
                    setContactGroupSyncMarker(account, FULLSYNC_MARKER);
                    syncResult.fullSyncRequested = true;
                    appSyncResult.setState(SyncResultState.SUCCESS);
                    return;
                default:
                    LogHelper.logI(TAG, "Ignoring unknown SyncState:" + result.syncstate);
                    break;
                }
            }

            boolean resync = processRestrictions(restr, result.restrictions, account);
            if (resync) {
                ContactManager.setDirtyFlag(mContext, account);
                syncResult.fullSyncRequested = true;
                appSyncResult.setState(SyncResultState.SUCCESS);
                return;
            }

            if (!dirtyGroups.isEmpty()) {
                ContactManager.clearGroupSyncFlags(mContext, dirtyGroups, result.newGroupIdMap, updatedGroupIds);
            }

            if (dirtyContacts != null && !dirtyContacts.isEmpty()) {
                ContactManager.clearSyncFlags(mContext, dirtyContacts, account.name, result.newContactIdMap,
                        updatedContactIds);
            }

            if (newIdMap != null && !newIdMap.isEmpty()) {
                ContactManager.clearServerId(mContext, newIdMap);
            }

            if (!syncContacts) {
                // if only groups were synced, restart for syncing contacts.
                syncResult.fullSyncRequested = true;
            }

            if (explizitPhotoSave) {
                // Clear Explizit Flag
                setExplicitSavePhoto(account, false);
            }

            appSyncResult.setState(SyncResultState.SUCCESS);
        } catch (final AuthenticatorException e) {
            LogHelper.logE(TAG, "AuthenticatorException", e);
            syncResult.stats.numParseExceptions++;
            setPrgErrorMsg(appSyncResult, e);
        } catch (final OperationCanceledException e) {
            LogHelper.logI(TAG, "OperationCanceledExcetpion", e);
            appSyncResult.setState(SyncResultState.AUTH_FAILED);
        } catch (final IOException e) {
            LogHelper.logE(TAG, "IOException", e);
            syncResult.stats.numIoExceptions++;
            setPrgErrorMsg(appSyncResult, e);
        } catch (final AuthenticationException e) {
            LogHelper.logI(TAG, "AuthenticationException", e);
            syncResult.stats.numAuthExceptions++;
            appSyncResult.setState(SyncResultState.AUTH_FAILED);
        } catch (final ParseException e) {
            LogHelper.logE(TAG, "ParseException", e);
            syncResult.stats.numParseExceptions++;
            setPrgErrorMsg(appSyncResult, e);
        } catch (InvalidKeyException e) {
            mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authtoken);
            ClientKeyHelper.clearPrivateKeyData(account, mAccountManager);
            LogHelper.logW(TAG, "InvalidKeyException", e);
            syncResult.stats.numAuthExceptions++;
        } catch (NetworkErrorException e) {
            syncResult.stats.numIoExceptions++;
            LogHelper.logWCause(TAG, "Sync failed because of a NetworkException", e);
            appSyncResult.setState(SyncResultState.NETWORK_ERROR);
            appSyncResult.setErrorMsg(String.format(getText(R.string.sync_network_error), e.getLocalizedMessage()));
        } catch (ServerException e) {
            syncResult.stats.numIoExceptions++;
            LogHelper.logWCause(TAG, "Sync failed because of a ServerException", e);
            appSyncResult.setState(SyncResultState.SERVER_ERROR);
            appSyncResult.setErrorMsg(getText(R.string.sync_server_error));
        } catch (OperationApplicationException e) {
            LogHelper.logW(TAG, "Sync failed because a DB-Operation failed", e);
            syncResult.databaseError = true;
            setPrgErrorMsg(appSyncResult, e);
        } catch (HeaderParseException e) {
            syncResult.stats.numIoExceptions++;
            LogHelper.logWCause(TAG, "Sync failed because of server reponse could not be parsed", e);
            setPrgErrorMsg(appSyncResult, e);
        } catch (HeaderCreateException e) {
            syncResult.databaseError = true;
            LogHelper.logE(TAG, "Sync failed because header could not be created", e);
            setPrgErrorMsg(appSyncResult, e);
        }
    }

    private void setPrgErrorMsg(AccountSyncResult appSyncResult, final Throwable e) {
        appSyncResult.setErrorMsg(String.format(mContext.getText(R.string.errormsg_programmingerror).toString(),
                e.getLocalizedMessage()));
    }

    /**
     * Update Clients Modifications but don't throws exceptions so that a sync
     * is not disturbed from a failure.
     * 
     * @param account
     */
    private void updateClientMod(Account account) {
        // Update ClientMod for concurrent changes
        try {
            ContactManager.updateClientModDate(mContext, account);
        } catch (IOException e) {
            LogHelper.logW(TAG, CLIENT_MOD_FAILED_MSG, e);
        } catch (OperationApplicationException e) {
            LogHelper.logE(TAG, CLIENT_MOD_FAILED_MSG, e);
        }
    }

    private boolean processRestrictions(Restrictions currRestr, Restrictions newRestr, Account account)
            throws OperationApplicationException {
        boolean resync = false;
        if (newRestr != null && (currRestr == null || !currRestr.equals(newRestr))) {
            // Notify User.
            NotificationManager notificationManager = (NotificationManager) mContext
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            if (currRestr != null && newRestr.isPhotoSyncSupported() && !currRestr.isPhotoSyncSupported()) {

                // Create ViewAccount-Intent
                Intent viewAccountsIntent = new Intent(mContext, ViewAccountsActivity.class);
                // Adds the back stack
                TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
                stackBuilder.addParentStack(ViewAccountsActivity.class);
                stackBuilder.addNextIntent(viewAccountsIntent);

                // Photo sync possible.
                NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext)
                        .setSmallIcon(Constants.NOTIF_ICON)
                        .setContentTitle(mContext.getText(R.string.notif_photosyncenabled_title).toString())
                        .setContentText(account.name).setAutoCancel(true).setOnlyAlertOnce(true)
                        .setContentIntent(stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT));
                notificationManager.notify(Constants.NOTIF_PHOTO_SYNC_SUPPORTED, mBuilder.build());
                // Resync to sync Photos
                resync = true;

                setExplicitSavePhoto(account, true);
            }
            if (newRestr.isPhotoSyncSupported()) {
                notificationManager.cancel(Constants.NOTIF_PHOTO_NOT_SYNCED);
            }
            if (!newRestr.isPhotoSyncSupported()) {
                notificationManager.cancel(Constants.NOTIF_PHOTO_SYNC_SUPPORTED);
            }

            if (currRestr != null && newRestr.getMaxContactCount() != currRestr.getMaxContactCount()) {
                // Create ViewAccount-Intent
                Intent viewAccountsIntent = new Intent(mContext, ViewAccountsActivity.class);
                // Adds the back stack
                TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
                stackBuilder.addParentStack(ViewAccountsActivity.class);
                stackBuilder.addNextIntent(viewAccountsIntent);
                boolean moreAllowed = newRestr.getMaxContactCount() > currRestr.getMaxContactCount();
                NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext)
                        .setSmallIcon(Constants.NOTIF_ICON)
                        .setContentTitle(moreAllowed ? getText(R.string.notif_contact_moresupported_title)
                                : getText(R.string.notif_contact_lesssupported_title))
                        .setContentText(String.format(getText(R.string.notif_contact_supported_content),
                                newRestr.getMaxContactCount(), account.name))
                        .setAutoCancel(true).setOnlyAlertOnce(true)
                        .setContentIntent(stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT));
                notificationManager.notify(Constants.NOTIF_MAX_CONTACT_SUPPORTED, mBuilder.build());
                // Hide Contacts not syned, but allow the message to show up
                // again if there are still more than allowed.
                notificationManager.cancel(Constants.NOTIF_CONTACTS_NOT_SYNCED);
                unhideMaxContactMessage(mContext, account);
            }

            if (resync) {
                ContactManager.setDirtyFlag(mContext, account);
            }

            SyncUtils.saveRestrictions(account, mAccountManager, newRestr);
        }
        return resync;
    }

    private String getText(int resId) {
        return mContext.getText(resId).toString();
    }

    private String checkIfSaltSaved(Account account, String authtoken) throws IOException,
            OperationCanceledException, AuthenticationException, NetworkErrorException, ServerException {
        String newAuthToken = authtoken;
        if (!ClientKeyHelper.isSaltSaved(account, mAccountManager)) {
            String saltStr = ClientKeyHelper.getSalt(account, mAccountManager);
            String pwdCheck = ClientKeyHelper.getPwdCheck(account, mAccountManager);
            if (saltStr != null && pwdCheck != null) {
                int retrycount = 1;
                boolean retry;
                do {
                    retry = false;
                    try {
                        boolean saltSaved = NetworkUtilities.savePwdSalt(mContext, account.name, newAuthToken,
                                saltStr, null, false, pwdCheck);
                        if (!saltSaved) {
                            mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, newAuthToken);
                            ClientKeyHelper.clearPrivateKeyData(account, mAccountManager);

                            return null;
                        }
                    } catch (AuthenticationException ex) {
                        LogHelper.logD(TAG, "Authentification failed, trying to get new Token", ex);
                        // Retry
                        mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, newAuthToken);
                        if (retrycount > 0) {
                            newAuthToken = NetworkUtilities.blockingGetAuthToken(mAccountManager, account, null);
                            if (newAuthToken != null) {
                                retry = true;
                            }
                            retrycount--;
                        } else {
                            throw ex;
                        }
                    }
                } while (retry);
                ClientKeyHelper.setSaltSaved(account, mAccountManager);
            } else {
                LogHelper.logW(TAG, "Salt or PwdCheck is null." + saltStr + " " + pwdCheck);
                // Only process if salt and pwd is saved
                return null;
            }
        }
        return newAuthToken;
    }

    /**
     * last syncAnchor for contact or 0 if we've never synced.
     * 
     * @param account
     *            the account we're syncing
     * @return last syncAnchor
     */
    private long getContactSyncMarker(Account account) {
        String markerString = mAccountManager.getUserData(account, SYNC_CONTACT_MARKER_KEY);
        if (!TextUtils.isEmpty(markerString)) {
            return Long.parseLong(markerString);
        }
        return 0;
    }

    private void setExplicitSavePhoto(Account account, boolean enabled) {
        mAccountManager.setUserData(account, SYNC_CONTACT_PHOTOSAVE_KEY, enabled ? Boolean.TRUE.toString() : null);
    }

    private boolean getExplicitSavePhoto(Account account) {
        String val = mAccountManager.getUserData(account, SYNC_CONTACT_PHOTOSAVE_KEY);
        return Boolean.parseBoolean(val);
    }

    /**
     * Save off the high-water-mark we receive back from the server.
     * 
     * @param account
     *            The account we're syncing
     * @param marker
     *            last syncAnchor
     */
    private void setContactSyncMarker(Account account, Long marker) {
        if (marker != null) {
            mAccountManager.setUserData(account, SYNC_CONTACT_MARKER_KEY, Long.toString(marker));
        }
    }

    /**
     * This helper function fetches the last known high-water-mark we received
     * from the server - or 0 if we've never synced.
     * 
     * @param account
     *            the account we're syncing
     * @return last syncAnchor or 0 n.a.
     */
    private long getContactGroupSyncMarker(Account account) {
        String markerString = mAccountManager.getUserData(account, SYNC_GROUP_MARKER_KEY);
        if (!TextUtils.isEmpty(markerString)) {
            return Long.parseLong(markerString);
        }
        return 0;
    }

    /**
     * Save the last syncAnchor for Group
     * 
     * @param account
     *            The account we're syncing
     * @param marker
     *            The high-water-mark we want to save.
     */
    private void setContactGroupSyncMarker(Account account, Long marker) {
        if (marker != null) {
            mAccountManager.setUserData(account, SYNC_GROUP_MARKER_KEY, Long.toString(marker));
        }
    }

    private void clearMissingKeyNotification(Account account) {
        NotificationManager mNotificationManager = (NotificationManager) mContext
                .getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancel(account.name, Constants.NOTIF_MISSING_KEY);
    }

    private void sendMissingKey(Account account, final String authToken, byte[] saltPwdCheck) {
        Intent intent = KeyPasswordActivity.createKeyPasswortActivity(mContext, account, authToken, saltPwdCheck);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        PendingIntent resultPendingIntent = PendingIntent.getActivity(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext)
                .setSmallIcon(R.drawable.notif_key)
                .setContentTitle(String.format(mContext.getText(R.string.notif_missing_key).toString()))
                .setContentText(account.name).setAutoCancel(false).setOnlyAlertOnce(true)
                .setContentIntent(resultPendingIntent);

        NotificationManager mNotificationManager = (NotificationManager) mContext
                .getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(account.name, Constants.NOTIF_MISSING_KEY, mBuilder.build());
    }

    private void notifyUserPhotoNotSynced(String accountName) {
        Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
        AccountManager acm = AccountManager.get(mContext);
        String synced = acm.getUserData(account, NOTIF_SHOWN_PHOTO_SYNCED);
        if (synced == null) {
            Intent shopIntent = new Intent(mContext, ShopActivity.class);
            shopIntent.putExtra(ShopActivity.PARM_ACCOUNT_NAME, accountName);
            TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
            stackBuilder.addParentStack(ShopActivity.class);
            stackBuilder.addNextIntent(shopIntent);

            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext)
                    .setSmallIcon(Constants.NOTIF_ICON)
                    .setContentTitle(getText(R.string.notif_photonotsynced_title)).setContentText(accountName)
                    .setAutoCancel(true)
                    .setContentIntent(stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
                    .setOnlyAlertOnce(true);
            NotificationManager mNotificationManager = (NotificationManager) mContext
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(Constants.NOTIF_PHOTO_NOT_SYNCED, mBuilder.build());
            acm.setUserData(account, NOTIF_SHOWN_PHOTO_SYNCED, String.valueOf(System.currentTimeMillis()));
        }
    }

    /**
     * Allows to show the Contact not synced message again
     * 
     * @param context
     * @param account
     */
    private static void unhideMaxContactMessage(Context context, Account account) {
        AccountManager acm = AccountManager.get(context);
        acm.setUserData(account, NOTIF_SHOWN_CONTACTS_SYNCED, null);
    }

    private void notifyUserConctactNotSynced(int maxCount, int totalLocalContacts, String accountName) {
        AccountManager acm = AccountManager.get(mContext);
        Account account = new Account(accountName, Constants.ACCOUNT_TYPE);
        String lastTimeShown = acm.getUserData(account, NOTIF_SHOWN_CONTACTS_SYNCED);
        Long lastTime;
        try {
            lastTime = lastTimeShown != null ? Long.parseLong(lastTimeShown) : null;
        } catch (NumberFormatException ex) {
            LogHelper.logWCause(TAG,
                    "Invalid Config-Settings:" + NOTIF_SHOWN_CONTACTS_SYNCED + " Value:" + lastTimeShown, ex);
            lastTime = null;
        }

        if (lastTime == null || System.currentTimeMillis() > lastTime.longValue() + NOTIF_WAIT_TIME) {
            // Create Shop-Intent
            Intent shopIntent = new Intent(mContext, ShopActivity.class);
            shopIntent.putExtra(ShopActivity.PARM_ACCOUNT_NAME, account.name);
            // Adds the back stack
            TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
            stackBuilder.addParentStack(ShopActivity.class);
            stackBuilder.addNextIntent(shopIntent);

            // Create Notification
            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext)
                    .setSmallIcon(Constants.NOTIF_ICON)
                    .setContentTitle(String.format(getText(R.string.notif_contactnotsynced_title), maxCount,
                            totalLocalContacts))
                    .setContentText(String.format(getText(R.string.notif_contactnotsynced_content), account.name))
                    .setAutoCancel(true).setOnlyAlertOnce(true)
                    .setContentIntent(stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT));
            NotificationManager mNotificationManager = (NotificationManager) mContext
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(Constants.NOTIF_CONTACTS_NOT_SYNCED, mBuilder.build());
            acm.setUserData(account, NOTIF_SHOWN_CONTACTS_SYNCED, String.valueOf(System.currentTimeMillis()));
        }
    }

    private final class SyncRestConflictHandler implements RestrictionConflictHandler {

        private final String accountName;

        private SyncRestConflictHandler(String accountName) {
            this.accountName = accountName;
        }

        public void onPhotoNotSynced(long rawContactId, long dataId) {
            notifyUserPhotoNotSynced(accountName);
        }

        public void onContactNotSynced(long rawContactId, int maxCount, int totalLocalContacts) {
            notifyUserConctactNotSynced(maxCount, totalLocalContacts, accountName);
        }
    }
}