com.samsung.android.remindme.SyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.samsung.android.remindme.SyncAdapter.java

Source

/*
 * Copyright 2010 Google Inc.
 *
 * 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.samsung.android.remindme;

import com.samsung.remindme.allshared.JsonRpcClient;
import com.samsung.remindme.allshared.JsonRpcException;
import com.samsung.remindme.allshared.RemindMeProtocol;
import com.samsung.android.remindme.ModelJava.DeviceRegistration;
import com.samsung.android.remindme.jsonrpc.AuthenticatedJsonRpcJavaClient;
import com.samsung.android.remindme.jsonrpc.AuthenticatedJsonRpcJavaClient.InvalidAuthTokenException;
import com.samsung.android.remindme.jsonrpc.AuthenticatedJsonRpcJavaClient.RequestedUserAuthenticationException;
import com.samsung.remindme.javashared.Util;
import com.google.android.c2dm.C2DMessaging;

import org.apache.http.auth.AuthenticationException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.content.SyncStats;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Toast;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * RemindMe SyncAdapter implementation. The sync adapter does the following:
 * <ul>
 *   <li>Device registration/unregistration when auto-sync settings for the account
 *     (or global settings) have changed, via the <code>devices.register</code> (and similar)
 *     RPC method.</li>
 *   <li>Checking for locally modified alerts since the last successful sync time.</li>
 *   <li>Synchronization with the server, via the <code>alerts.sync</code> RPC method.</li>
 * </ul>
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {
    static final String TAG = Config.makeLogTag(SyncAdapter.class);

    public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
    public static final String[] GOOGLE_ACCOUNT_REQUIRED_SYNCABILITY_FEATURES = new String[] { "service_ah" };

    public static final String DEVICE_TYPE = "android";
    public static final String LAST_SYNC = "last_sync";
    public static final String SERVER_LAST_SYNC = "server_last_sync";
    public static final String DM_REGISTERED = "dm_registered";

    private static final String[] PROJECTION = new String[] { RemindMeContract.Alerts._ID, // 0
            RemindMeContract.Alerts.SERVER_ID, // 1
            RemindMeContract.Alerts.TITLE, // 2
            RemindMeContract.Alerts.BODY, // 3
            RemindMeContract.Alerts.CREATED_DATE, // 4
            RemindMeContract.Alerts.MODIFIED_DATE, // 5
            RemindMeContract.Alerts.PENDING_DELETE, // 6
    };

    private final Context mContext;

    public SyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mContext = context;
    }

    @Override
    public void onPerformSync(final Account account, Bundle extras, String authority,
            final ContentProviderClient provider, final SyncResult syncResult) {
        Log.i(TAG, "onPerformSync called!");
        TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        String clientDeviceId = tm.getDeviceId();

        final long newSyncTime = System.currentTimeMillis();

        final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
        final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
        final boolean initialize = extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);

        C2DMReceiver.refreshAppC2DMRegistrationState(mContext);

        Log.i(TAG, "Beginning " + (uploadOnly ? "upload-only" : "full") + " sync for account " + account.name);

        // Read this account's sync metadata
        final SharedPreferences syncMeta = mContext.getSharedPreferences("sync:" + account.name, 0);
        long lastSyncTime = syncMeta.getLong(LAST_SYNC, 0);
        long lastServerSyncTime = syncMeta.getLong(SERVER_LAST_SYNC, 0);

        // Check for changes in either app-wide auto sync registration information, or changes in
        // the user's preferences for auto sync on this account; if either changes, piggy back the
        // new registration information in this sync.
        long lastRegistrationChangeTime = C2DMessaging.getLastRegistrationChange(mContext);

        boolean autoSyncDesired = ContentResolver.getMasterSyncAutomatically()
                && ContentResolver.getSyncAutomatically(account, RemindMeContract.AUTHORITY);
        boolean autoSyncEnabled = syncMeta.getBoolean(DM_REGISTERED, false);

        // Will be 0 for no change, -1 for unregister, 1 for register.
        final int deviceRegChange;
        JsonRpcClient.Call deviceRegCall = null;
        if (autoSyncDesired != autoSyncEnabled || lastRegistrationChangeTime > lastSyncTime || initialize
                || manualSync) {

            String registrationId = C2DMessaging.getRegistrationId(mContext);
            deviceRegChange = (autoSyncDesired && registrationId != null) ? 1 : -1;

            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG,
                        "Auto sync selection or registration information has changed, "
                                + (deviceRegChange == 1 ? "registering" : "unregistering")
                                + " messaging for this device, for account " + account.name);
            }

            try {
                if (deviceRegChange == 1) {
                    // Register device for auto sync on this account.
                    deviceRegCall = new JsonRpcClient.Call(RemindMeProtocol.DevicesRegister.METHOD);
                    JSONObject params = new JSONObject();

                    DeviceRegistration device = new DeviceRegistration(clientDeviceId, DEVICE_TYPE, registrationId);
                    params.put(RemindMeProtocol.DevicesRegister.ARG_DEVICE, device.toJSON());
                    deviceRegCall.setParams(params);
                } else {
                    // Unregister device for auto sync on this account.
                    deviceRegCall = new JsonRpcClient.Call(RemindMeProtocol.DevicesUnregister.METHOD);
                    JSONObject params = new JSONObject();
                    params.put(RemindMeProtocol.DevicesUnregister.ARG_DEVICE_ID, clientDeviceId);
                    deviceRegCall.setParams(params);
                }
            } catch (JSONException e) {
                logErrorMessage("Error generating device registration remote RPC parameters.", manualSync);
                e.printStackTrace();
                return;
            }
        } else {
            deviceRegChange = 0;
        }

        // Get the list of locally changed alerts. If this is an upload-only sync and there were
        // no local changes, cancel the sync.
        List<ModelJava.Alert> locallyChangedAlerts = null;
        try {
            locallyChangedAlerts = getLocallyChangedAlerts(provider, account, new Date(lastSyncTime));
        } catch (RemoteException e) {
            logErrorMessage("Remote exception accessing content provider: " + e.getMessage(), manualSync);
            e.printStackTrace();
            syncResult.stats.numIoExceptions++;
            return;
        }

        if (uploadOnly && locallyChangedAlerts.isEmpty() && deviceRegCall == null) {
            Log.i(TAG, "No local changes; upload-only sync canceled.");
            return;
        }

        // Set up the RPC sync calls
        final AuthenticatedJsonRpcJavaClient jsonRpcClient = new AuthenticatedJsonRpcJavaClient(mContext,
                Config.SERVER_AUTH_URL_TEMPLATE, Config.SERVER_RPC_URL);
        try {
            jsonRpcClient.blockingAuthenticateAccount(account,
                    manualSync ? AuthenticatedJsonRpcJavaClient.NEED_AUTH_INTENT
                            : AuthenticatedJsonRpcJavaClient.NEED_AUTH_NOTIFICATION,
                    false);
        } catch (AuthenticationException e) {
            logErrorMessage("Authentication exception when attempting to sync. root cause: " + e.getMessage(),
                    manualSync);
            e.printStackTrace();

            syncResult.stats.numAuthExceptions++;
            return;
        } catch (OperationCanceledException e) {
            Log.i(TAG, "Sync for account " + account.name + " manually canceled.");
            return;
        } catch (RequestedUserAuthenticationException e) {
            syncResult.stats.numAuthExceptions++;
            return;
        } catch (InvalidAuthTokenException e) {
            logErrorMessage("Invalid auth token provided by AccountManager when attempting to " + "sync.",
                    manualSync);
            e.printStackTrace();
            syncResult.stats.numAuthExceptions++;
            return;
        }

        // Set up the alerts sync call.
        JsonRpcClient.Call alertsSyncCall = new JsonRpcClient.Call(RemindMeProtocol.AlertsSync.METHOD);
        try {
            JSONObject params = new JSONObject();
            params.put(RemindMeProtocol.ARG_CLIENT_DEVICE_ID, clientDeviceId);
            params.put(RemindMeProtocol.AlertsSync.ARG_SINCE_DATE,
                    Util.formatDateISO8601(new Date(lastServerSyncTime)));

            JSONArray locallyChangedAlertsJson = new JSONArray();
            for (ModelJava.Alert locallyChangedAlert : locallyChangedAlerts) {
                locallyChangedAlertsJson.put(locallyChangedAlert.toJSON());
            }

            params.put(RemindMeProtocol.AlertsSync.ARG_LOCAL_NOTES, locallyChangedAlertsJson);
            alertsSyncCall.setParams(params);
        } catch (JSONException e) {
            logErrorMessage("Error generating sync remote RPC parameters.", manualSync);
            e.printStackTrace();
            syncResult.stats.numParseExceptions++;
            return;
        }

        List<JsonRpcClient.Call> jsonRpcCalls = new ArrayList<JsonRpcClient.Call>();
        jsonRpcCalls.add(alertsSyncCall);
        if (deviceRegChange != 0)
            jsonRpcCalls.add(deviceRegCall);

        jsonRpcClient.callBatch(jsonRpcCalls, new JsonRpcClient.BatchCallback() {
            public void onData(Object[] data) {
                if (data[0] != null) {
                    // Read alerts sync data.
                    JSONObject dataJson = (JSONObject) data[0];
                    try {
                        List<ModelJava.Alert> changedAlerts = new ArrayList<ModelJava.Alert>();
                        JSONArray alertsJson = dataJson.getJSONArray(RemindMeProtocol.AlertsSync.RET_NOTES);
                        for (int i = 0; i < alertsJson.length(); i++) {
                            changedAlerts.add(new ModelJava.Alert(alertsJson.getJSONObject(i)));
                        }

                        reconcileSyncedAlerts(provider, account, changedAlerts, syncResult.stats);

                        // If sync is successful (no exceptions thrown), update sync metadata
                        long newServerSyncTime = Util
                                .parseDateISO8601(
                                        dataJson.getString(RemindMeProtocol.AlertsSync.RET_NEW_SINCE_DATE))
                                .getTime();
                        syncMeta.edit().putLong(LAST_SYNC, newSyncTime).commit();
                        syncMeta.edit().putLong(SERVER_LAST_SYNC, newServerSyncTime).commit();
                        Log.i(TAG, "Sync complete, setting last sync time to " + Long.toString(newSyncTime));
                    } catch (JSONException e) {
                        logErrorMessage("Error parsing alert sync RPC response", manualSync);
                        e.printStackTrace();
                        syncResult.stats.numParseExceptions++;
                        return;
                    } catch (ParseException e) {
                        logErrorMessage("Error parsing alert sync RPC response", manualSync);
                        e.printStackTrace();
                        syncResult.stats.numParseExceptions++;
                        return;
                    } catch (RemoteException e) {
                        logErrorMessage("RemoteException in reconcileSyncedAlerts: " + e.getMessage(), manualSync);
                        e.printStackTrace();
                        return;
                    } catch (OperationApplicationException e) {
                        logErrorMessage("Could not apply batch operations to content provider: " + e.getMessage(),
                                manualSync);
                        e.printStackTrace();
                        return;
                    } finally {
                        provider.release();
                    }
                }

                // Read device reg data.
                if (deviceRegChange != 0) {
                    // data[1] will be null in case of an error (successful unregisters
                    // will have an empty JSONObject, not null).
                    boolean registered = (data[1] != null && deviceRegChange == 1);
                    syncMeta.edit().putBoolean(DM_REGISTERED, registered).commit();
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "Stored account auto sync registration state: " + Boolean.toString(registered));
                    }
                }
            }

            public void onError(int callIndex, JsonRpcException e) {
                if (e.getHttpCode() == 403) {
                    Log.w(TAG, "Got a 403 response, invalidating App Engine ACSID token");
                    jsonRpcClient.invalidateAccountAcsidToken(account);
                }

                provider.release();
                logErrorMessage("Error calling remote alert sync RPC", manualSync);
                e.printStackTrace();
            }
        });
    }

    public void reconcileSyncedAlerts(ContentProviderClient provider, Account account,
            List<ModelJava.Alert> changedAlerts, SyncStats syncStats)
            throws RemoteException, OperationApplicationException {
        Cursor alertCursor;
        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
        for (ModelJava.Alert changedAlert : changedAlerts) {
            Uri alertUri = null;

            if (changedAlert.getId() != null) {
                alertUri = addCallerIsSyncAdapterParameter(
                        RemindMeContract.buildAlertUri(account.name, Long.parseLong(changedAlert.getId())));
            } else {
                alertCursor = provider.query(RemindMeContract.buildAlertListUri(account.name), PROJECTION,
                        RemindMeContract.Alerts.SERVER_ID + " = ?", new String[] { changedAlert.getServerId() },
                        null);

                if (alertCursor.moveToNext()) {
                    alertUri = addCallerIsSyncAdapterParameter(
                            RemindMeContract.buildAlertUri(account.name, alertCursor.getLong(0)));
                }

                alertCursor.close();
            }

            if (changedAlert.isPendingDelete()) {
                // Handle server-side delete.
                if (alertUri != null) {
                    operations.add(ContentProviderOperation.newDelete(alertUri).build());
                    syncStats.numDeletes++;
                }
            } else {
                ContentValues values = changedAlert.toContentValues();
                if (alertUri != null) {
                    // Handle server-side update.
                    operations.add(ContentProviderOperation.newUpdate(alertUri).withValues(values).build());
                    syncStats.numUpdates++;
                } else {
                    // Handle server-side insert.
                    operations
                            .add(ContentProviderOperation
                                    .newInsert(addCallerIsSyncAdapterParameter(
                                            RemindMeContract.buildAlertListUri(account.name)))
                                    .withValues(values).build());
                    syncStats.numInserts++;
                }
            }
        }

        provider.applyBatch(operations);
    }

    public List<ModelJava.Alert> getLocallyChangedAlerts(ContentProviderClient provider, Account account,
            Date sinceDate) throws RemoteException {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Getting local alerts changed since " + Long.toString(sinceDate.getTime()));
        }
        Cursor alertsCursor = provider.query(RemindMeContract.buildAlertListUri(account.name), PROJECTION,
                RemindMeContract.Alerts.MODIFIED_DATE + " > ?", new String[] { Long.toString(sinceDate.getTime()) },
                null);

        List<ModelJava.Alert> locallyChangedAlerts = new ArrayList<ModelJava.Alert>();
        while (alertsCursor.moveToNext()) {
            ContentValues values = new ContentValues();
            DatabaseUtils.cursorRowToContentValues(alertsCursor, values);
            ModelJava.Alert changedAlert = new ModelJava.Alert(values);
            locallyChangedAlerts.add(changedAlert);
        }

        alertsCursor.close();
        return locallyChangedAlerts;
    }

    private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
        return uri.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
    }

    public static void clearSyncData(Context context) {
        AccountManager am = AccountManager.get(context);
        Account[] accounts = am.getAccounts();
        for (Account account : accounts) {
            final SharedPreferences syncMeta = context.getSharedPreferences("sync:" + account.name, 0);
            syncMeta.edit().clear().commit();
        }
    }

    private void logErrorMessage(final String message, boolean showToast) {
        Log.e(TAG, message);
        System.out.println(message);
        // Alert: in general, showing any form of UI from a service is bad. showToast should only
        // be true if this is a manual sync, i.e. the user has just invoked some UI that indicates
        // she wants to perform a sync.
        Looper mainLooper = mContext.getMainLooper();
        if (mainLooper != null) {
            new Handler(mainLooper).post(new Runnable() {
                public void run() {
                    Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
}