package edu.mit.mobile.android.locast.sync; /* * Copyright (C) 2011 MIT Mobile Experience Lab * * 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import java.io.IOException; import java.io.InterruptedIOException; import java.lang.ref.WeakReference; import java.util.HashMap; import org.json.JSONException; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; import android.app.Service; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.OperationApplicationException; import android.content.SyncResult; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import edu.mit.mobile.android.locast.Constants; import edu.mit.mobile.android.locast.accounts.Authenticator; import edu.mit.mobile.android.locast.data.Cast; import edu.mit.mobile.android.locast.data.Itinerary; import edu.mit.mobile.android.locast.data.MediaProvider; import edu.mit.mobile.android.locast.data.NoPublicPath; import edu.mit.mobile.android.locast.data.SyncException; import edu.mit.mobile.android.locast.net.NetworkClient; import edu.mit.mobile.android.locast.net.NetworkProtocolException; /** * A wrapper to {@link SyncEngine} which provides the interface to the * {@link ContentResolver} sync framework. * * There are some helper static methods to simplify the creation of the * {@link ContentResolver#requestSync(Account, String, Bundle)} calls. See * {@link #startSync(Account, Uri, boolean, Bundle)} and friends. * * @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a> * */ public class LocastSyncService extends Service { private static final String TAG = LocastSyncService.class.getSimpleName(); private static final boolean DEBUG = Constants.DEBUG; private static LocastSyncAdapter SYNC_ADAPTER = null; /** * A string extra specifying the URI of the object to sync. Can be a * content:// or http:// uri. */ public static final String EXTRA_SYNC_URI = "edu.mit.mobile.android.locast.sync.EXTRA_SYNC_URI"; /** * Convenience method to start a background sync of the first account found. * * @param context * @param what * @see #startSync(Account, Uri, boolean, Bundle) */ public static void startSync(Context context, Uri what) { startSync(Authenticator.getFirstAccount(context), what, false, new Bundle()); } /** * Convenience method to start a sync of the first account found. * * @param context * @param what * @param explicitSync * @see #startSync(Account, Uri, boolean, Bundle) */ public static void startSync(Context context, Uri what, boolean explicitSync) { startSync(Authenticator.getFirstAccount(context), what, explicitSync, new Bundle()); } /** * @param context * @param what * @param explicitSync * @param extras * @see #startSync(Account, Uri, boolean, Bundle) */ public static void startSync(Context context, Uri what, boolean explicitSync, Bundle extras) { startSync(Authenticator.getFirstAccount(context), what, explicitSync, extras); } public static void startExpeditedAutomaticSync(Context context, Uri what){ final Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); startSync(Authenticator.getFirstAccount(context), what, false, extras); } /** * Requests a sync of the item specified by a public URL. * * @param context * @param httpPubUrl * an http or https URL pointing to a json array of json objects * (if {@code destination} is a dir uri) or a single json object. * @param destination * the destination local uri for the result to be stored in if * it's not present * @param explicitSync * @see #startSync(Account, Uri, boolean, Bundle) */ public static void startSync(Context context, Uri httpPubUrl, Uri destination, boolean explicitSync) { final Bundle b = new Bundle(); b.putString(SyncEngine.EXTRA_DESTINATION_URI, destination.toString()); startSync(Authenticator.getFirstAccount(context), httpPubUrl, explicitSync, b); } /** * Convenience method to request a sync. * * @param account * @param what * @param explicitSync * @see #startSync(Account, Uri, boolean, Bundle) */ public static void startSync(Account account, Uri what, boolean explicitSync) { final Bundle b = new Bundle(); startSync(account, what, explicitSync, b); } /** * Convenience method to request a sync. * * This wraps {@link ContentResolver#requestSync(Account, String, Bundle)} * and fills in the necessary extras. * * @param account * @param what the uri of the item that needs to be sync'd. can be null * @param explicitSync * if true, adds {@link ContentResolver#SYNC_EXTRAS_MANUAL} to * the extras * @param extras */ public static void startSync(Account account, Uri what, boolean explicitSync, Bundle extras) { if (explicitSync) { extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_EXPEDITED)) { extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } } if (what != null){ extras.putString(LocastSyncService.EXTRA_SYNC_URI, what.toString()); } if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)){ if (DEBUG) { Log.d(TAG, "canceling current sync to make room for expedited sync of " + what); } ContentResolver.cancelSync(account, MediaProvider.AUTHORITY); } if (DEBUG){ Log.d(TAG, "requesting sync for "+account + " with extras: "+extras); } ContentResolver.requestSync(account, MediaProvider.AUTHORITY, extras); } @Override public IBinder onBind(Intent intent) { return getSyncAdapter().getSyncAdapterBinder(); } @Override public void onCreate() { super.onCreate(); AccountManager.get(this).addOnAccountsUpdatedListener(getSyncAdapter(), null, true); } @Override public void onDestroy() { super.onDestroy(); AccountManager.get(this).removeOnAccountsUpdatedListener(getSyncAdapter()); } private static class LocastSyncAdapter extends AbstractThreadedSyncAdapter implements OnAccountsUpdateListener { private final Context mContext; private final HashMap<Account, SyncEngine> mSyncEngines = new HashMap<Account, SyncEngine>(); private WeakReference<Thread> mSyncThread; private Account mCurrentlySyncing; public LocastSyncAdapter(Context context) { super(context, true); mContext = context; } @Override public void onSyncCanceled() { if (DEBUG){ Log.d(TAG, "onSyncCanceled()"); } super.onSyncCanceled(); if (mSyncThread != null){ final Thread syncThread = mSyncThread.get(); if (syncThread != null){ Log.d(TAG, "interrupting current sync thread "+syncThread.getId()+"..."); syncThread.interrupt(); Log.d(TAG, "waiting for previous sync to finish..."); try { syncThread.join(); } catch (final InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { mCurrentlySyncing = account; SyncEngine syncEngine = mSyncEngines.get(account); if (syncEngine == null) { syncEngine = new SyncEngine(mContext, NetworkClient.getInstance(mContext, account)); mSyncEngines.put(account, syncEngine); } mSyncThread = new WeakReference<Thread>(Thread.currentThread()); final String uriString = extras.getString(EXTRA_SYNC_URI); final Uri uri = uriString != null ? Uri.parse(uriString) : null; if (uri != null) { extras.remove(EXTRA_SYNC_URI); } final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); if (DEBUG){ if (uri != null) { Log.d(TAG, "onPerformSync() triggered with uri: " + uri); } else { Log.d(TAG, "onPerformSync() triggered without an uri."); } } try { if (uri != null) { syncEngine.sync(uri, account, extras, provider, syncResult); } else { if (uploadOnly){ syncEngine.uploadUnpublished(Cast.CONTENT_URI, account, extras, provider, syncResult); }else{ syncEngine.sync(Cast.FEATURED, account, extras, provider, syncResult); syncEngine.sync(Itinerary.CONTENT_URI, account, extras, provider, syncResult); if (!Authenticator.isDemoMode(mContext)){ syncEngine.sync(Cast.FAVORITE, account, extras, provider, syncResult); } } } } catch (final InterruptedIOException e) { if (DEBUG) { Log.i(TAG, "Sync was interrupted"); } } catch (final InterruptedException e) { if (DEBUG) { Log.i(TAG, "Sync was interrupted"); } } catch (final RemoteException e) { Log.e(TAG, e.toString(), e); // TODO handle } catch (final SyncException e) { Log.e(TAG, e.toString(), e); // TODO handle } catch (final JSONException e) { syncResult.stats.numParseExceptions++; Log.e(TAG, e.toString(), e); } catch (final IOException e) { syncResult.stats.numIoExceptions++; Log.e(TAG, e.toString(), e); } catch (final NetworkProtocolException e) { syncResult.stats.numParseExceptions++; Log.e(TAG, e.toString(), e); } catch (final NoPublicPath e) { Log.e(TAG, e.toString(), e); } catch (final OperationApplicationException e) { Log.e(TAG, e.toString(), e); // TODO handle } catch ( final SQLiteException e){ syncResult.databaseError = true; Log.e(TAG, e.toString(), e); } catch (final IllegalArgumentException e){ syncResult.databaseError = true; Log.e(TAG, e.toString(), e); } finally { mCurrentlySyncing = null; } } // onPerformSync @Override public void onAccountsUpdated(Account[] accounts) { for (final Account cachedEngines : mSyncEngines.keySet()) { boolean accountStillExists = false; for (final Account account : accounts) { if (cachedEngines.equals(account)) { accountStillExists = true; break; } } if (!accountStillExists) { if (DEBUG) { Log.d(TAG, "removing stale sync engine for removed account " + cachedEngines); mSyncEngines.remove(cachedEngines); } } } } } // LocastSyncAdapter private LocastSyncAdapter getSyncAdapter() { if (SYNC_ADAPTER == null) { SYNC_ADAPTER = new LocastSyncAdapter(this); } return SYNC_ADAPTER; } }