/*
* 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();
}
});
}
}
}