rpicms-android

Android app for the RPICMS.
git clone git://archive.git.mtrnord.blog/RpicmsTeam/rpicms-android.git
Log | Files | Refs

SyncAdapter.java (13572B)


      1 package de.nordgedanken.rpicms.basicsyncadapter;
      2 
      3 /**
      4  * Created by frank on 13.10.14.
      5  */
      6 import android.accounts.Account;
      7 import android.annotation.TargetApi;
      8 import android.content.AbstractThreadedSyncAdapter;
      9 import android.content.ContentProviderClient;
     10 import android.content.ContentProviderOperation;
     11 import android.content.ContentResolver;
     12 import android.content.Context;
     13 import android.content.OperationApplicationException;
     14 import android.content.SyncResult;
     15 import android.database.Cursor;
     16 import android.net.Uri;
     17 import android.os.Build;
     18 import android.os.Bundle;
     19 import android.os.RemoteException;
     20 import android.util.Log;
     21 
     22 import de.nordgedanken.rpicms.basicsyncadapter.net.FeedParser;
     23 import de.nordgedanken.rpicms.basicsyncadapter.provider.FeedContract;
     24 
     25 import org.xmlpull.v1.XmlPullParserException;
     26 
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.net.HttpURLConnection;
     30 import java.net.MalformedURLException;
     31 import java.net.URL;
     32 import java.text.ParseException;
     33 import java.util.ArrayList;
     34 import java.util.HashMap;
     35 import java.util.List;
     36 class SyncAdapter extends AbstractThreadedSyncAdapter {
     37 
     38 
     39 
     40     /**
     41      * Define a sync adapter for the app.
     42      *
     43      * <p>This class is instantiated in {@link SyncService}, which also binds SyncAdapter to the system.
     44      * SyncAdapter should only be initialized in SyncService, never anywhere else.
     45      *
     46      * <p>The system calls onPerformSync() via an RPC call through the IBinder object supplied by
     47      * SyncService.
     48      */
     49 
     50         public static final String TAG = "SyncAdapter";
     51 
     52         /**
     53          * URL to fetch content from during a sync.
     54          *
     55          * <p>This points to the Android Developers Blog. (Side note: We highly recommend reading the
     56          * Android Developer Blog to stay up to date on the latest Android platform developments!)
     57          */
     58         private static final String FEED_URL = "http://nordgedanken.de/feed/atom";
     59 
     60         /**
     61          * Network connection timeout, in milliseconds.
     62          */
     63         private static final int NET_CONNECT_TIMEOUT_MILLIS = 15000;  // 15 seconds
     64 
     65         /**
     66          * Network read timeout, in milliseconds.
     67          */
     68         private static final int NET_READ_TIMEOUT_MILLIS = 10000;  // 10 seconds
     69 
     70         /**
     71          * Content resolver, for performing database operations.
     72          */
     73         private final ContentResolver mContentResolver;
     74 
     75         /**
     76          * Project used when querying content provider. Returns all known fields.
     77          */
     78         private static final String[] PROJECTION = new String[] {
     79                 FeedContract.Entry._ID,
     80                 FeedContract.Entry.COLUMN_NAME_ENTRY_ID,
     81                 FeedContract.Entry.COLUMN_NAME_TITLE,
     82                 FeedContract.Entry.COLUMN_NAME_LINK,
     83                 FeedContract.Entry.COLUMN_NAME_PUBLISHED};
     84 
     85         // Constants representing column positions from PROJECTION.
     86         public static final int COLUMN_ID = 0;
     87         public static final int COLUMN_ENTRY_ID = 1;
     88         public static final int COLUMN_TITLE = 2;
     89         public static final int COLUMN_LINK = 3;
     90         public static final int COLUMN_PUBLISHED = 4;
     91 
     92         /**
     93          * Constructor. Obtains handle to content resolver for later use.
     94          */
     95         public SyncAdapter(Context context, boolean autoInitialize) {
     96             super(context, autoInitialize);
     97             mContentResolver = context.getContentResolver();
     98         }
     99 
    100         /**
    101          * Constructor. Obtains handle to content resolver for later use.
    102          */
    103         @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    104         public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
    105             super(context, autoInitialize, allowParallelSyncs);
    106             mContentResolver = context.getContentResolver();
    107         }
    108 
    109         /**
    110          * Called by the Android system in response to a request to run the sync adapter. The work
    111          * required to read data from the network, parse it, and store it in the content provider is
    112          * done here. Extending AbstractThreadedSyncAdapter ensures that all methods within SyncAdapter
    113          * run on a background thread. For this reason, blocking I/O and other long-running tasks can be
    114          * run <em>in situ</em>, and you don't have to set up a separate thread for them.
    115          .
    116          *
    117          * <p>This is where we actually perform any work required to perform a sync.
    118          * {@link android.content.AbstractThreadedSyncAdapter} guarantees that this will be called on a non-UI thread,
    119          * so it is safe to peform blocking I/O here.
    120          *
    121          * <p>The syncResult argument allows you to pass information back to the method that triggered
    122          * the sync.
    123          */
    124         @Override
    125         public void onPerformSync(Account account, Bundle extras, String authority,
    126                                   ContentProviderClient provider, SyncResult syncResult) {
    127             Log.i(TAG, "Beginning network synchronization");
    128             try {
    129                 final URL location = new URL(FEED_URL);
    130                 InputStream stream = null;
    131 
    132                 try {
    133                     Log.i(TAG, "Streaming data from network: " + location);
    134                     stream = downloadUrl(location);
    135                     updateLocalFeedData(stream, syncResult);
    136                     // Makes sure that the InputStream is closed after the app is
    137                     // finished using it.
    138                 } finally {
    139                     if (stream != null) {
    140                         stream.close();
    141                     }
    142                 }
    143             } catch (MalformedURLException e) {
    144                 Log.e(TAG, "Feed URL is malformed", e);
    145                 syncResult.stats.numParseExceptions++;
    146                 return;
    147             } catch (IOException e) {
    148                 Log.e(TAG, "Error reading from network: " + e.toString());
    149                 syncResult.stats.numIoExceptions++;
    150                 return;
    151             } catch (XmlPullParserException e) {
    152                 Log.e(TAG, "Error parsing feed: " + e.toString());
    153                 syncResult.stats.numParseExceptions++;
    154                 return;
    155             } catch (ParseException e) {
    156                 Log.e(TAG, "Error parsing feed: " + e.toString());
    157                 syncResult.stats.numParseExceptions++;
    158                 return;
    159             } catch (RemoteException e) {
    160                 Log.e(TAG, "Error updating database: " + e.toString());
    161                 syncResult.databaseError = true;
    162                 return;
    163             } catch (OperationApplicationException e) {
    164                 Log.e(TAG, "Error updating database: " + e.toString());
    165                 syncResult.databaseError = true;
    166                 return;
    167             }
    168             Log.i(TAG, "Network synchronization complete");
    169         }
    170 
    171         /**
    172          * Read XML from an input stream, storing it into the content provider.
    173          *
    174          * <p>This is where incoming data is persisted, committing the results of a sync. In order to
    175          * minimize (expensive) disk operations, we compare incoming data with what's already in our
    176          * database, and compute a merge. Only changes (insert/update/delete) will result in a database
    177          * write.
    178          *
    179          * <p>As an additional optimization, we use a batch operation to perform all database writes at
    180          * once.
    181          *
    182          * <p>Merge strategy:
    183          * 1. Get cursor to all items in feed<br/>
    184          * 2. For each item, check if it's in the incoming data.<br/>
    185          *    a. YES: Remove from "incoming" list. Check if data has mutated, if so, perform
    186          *            database UPDATE.<br/>
    187          *    b. NO: Schedule DELETE from database.<br/>
    188          * (At this point, incoming database only contains missing items.)<br/>
    189          * 3. For any items remaining in incoming list, ADD to database.
    190          */
    191         public void updateLocalFeedData(final InputStream stream, final SyncResult syncResult)
    192                 throws IOException, XmlPullParserException, RemoteException,
    193                 OperationApplicationException, ParseException {
    194             final FeedParser feedParser = new FeedParser();
    195             final ContentResolver contentResolver = getContext().getContentResolver();
    196 
    197             Log.i(TAG, "Parsing stream as Atom feed");
    198             final List<FeedParser.Entry> entries = feedParser.parse(stream);
    199             Log.i(TAG, "Parsing complete. Found " + entries.size() + " entries");
    200 
    201 
    202             ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();
    203 
    204             // Build hash table of incoming entries
    205             HashMap<String, FeedParser.Entry> entryMap = new HashMap<String, FeedParser.Entry>();
    206             for (FeedParser.Entry e : entries) {
    207                 entryMap.put(e.id, e);
    208             }
    209 
    210             // Get list of all items
    211             Log.i(TAG, "Fetching local entries for merge");
    212             Uri uri = FeedContract.Entry.CONTENT_URI; // Get all entries
    213             Cursor c = contentResolver.query(uri, PROJECTION, null, null, null);
    214             assert c != null;
    215             Log.i(TAG, "Found " + c.getCount() + " local entries. Computing merge solution...");
    216 
    217             // Find stale data
    218             int id;
    219             String entryId;
    220             String title;
    221             String link;
    222             long published;
    223             while (c.moveToNext()) {
    224                 syncResult.stats.numEntries++;
    225                 id = c.getInt(COLUMN_ID);
    226                 entryId = c.getString(COLUMN_ENTRY_ID);
    227                 title = c.getString(COLUMN_TITLE);
    228                 link = c.getString(COLUMN_LINK);
    229                 published = c.getLong(COLUMN_PUBLISHED);
    230                 FeedParser.Entry match = entryMap.get(entryId);
    231                 if (match != null) {
    232                     // Entry exists. Remove from entry map to prevent insert later.
    233                     entryMap.remove(entryId);
    234                     // Check to see if the entry needs to be updated
    235                     Uri existingUri = FeedContract.Entry.CONTENT_URI.buildUpon()
    236                             .appendPath(Integer.toString(id)).build();
    237                     if ((match.title != null && !match.title.equals(title)) ||
    238                             (match.link != null && !match.link.equals(link)) ||
    239                             (match.published != published)) {
    240                         // Update existing record
    241                         Log.i(TAG, "Scheduling update: " + existingUri);
    242                         batch.add(ContentProviderOperation.newUpdate(existingUri)
    243                                 .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, title)
    244                                 .withValue(FeedContract.Entry.COLUMN_NAME_LINK, link)
    245                                 .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, published)
    246                                 .build());
    247                         syncResult.stats.numUpdates++;
    248                     } else {
    249                         Log.i(TAG, "No action: " + existingUri);
    250                     }
    251                 } else {
    252                     // Entry doesn't exist. Remove it from the database.
    253                     Uri deleteUri = FeedContract.Entry.CONTENT_URI.buildUpon()
    254                             .appendPath(Integer.toString(id)).build();
    255                     Log.i(TAG, "Scheduling delete: " + deleteUri);
    256                     batch.add(ContentProviderOperation.newDelete(deleteUri).build());
    257                     syncResult.stats.numDeletes++;
    258                 }
    259             }
    260             c.close();
    261 
    262             // Add new items
    263             for (FeedParser.Entry e : entryMap.values()) {
    264                 Log.i(TAG, "Scheduling insert: entry_id=" + e.id);
    265                 batch.add(ContentProviderOperation.newInsert(FeedContract.Entry.CONTENT_URI)
    266                         .withValue(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, e.id)
    267                         .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, e.title)
    268                         .withValue(FeedContract.Entry.COLUMN_NAME_LINK, e.link)
    269                         .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, e.published)
    270                         .build());
    271                 syncResult.stats.numInserts++;
    272             }
    273             Log.i(TAG, "Merge solution ready. Applying batch update");
    274             mContentResolver.applyBatch(FeedContract.CONTENT_AUTHORITY, batch);
    275             mContentResolver.notifyChange(
    276                     FeedContract.Entry.CONTENT_URI, // URI where data was modified
    277                     null,                           // No local observer
    278                     false);                         // IMPORTANT: Do not sync to network
    279             // This sample doesn't support uploads, but if *your* code does, make sure you set
    280             // syncToNetwork=false in the line above to prevent duplicate syncs.
    281         }
    282 
    283         /**
    284          * Given a string representation of a URL, sets up a connection and gets an input stream.
    285          */
    286         private InputStream downloadUrl(final URL url) throws IOException {
    287             HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    288             conn.setReadTimeout(NET_READ_TIMEOUT_MILLIS /* milliseconds */);
    289             conn.setConnectTimeout(NET_CONNECT_TIMEOUT_MILLIS /* milliseconds */);
    290             conn.setRequestMethod("GET");
    291             conn.setDoInput(true);
    292             // Starts the query
    293             conn.connect();
    294             return conn.getInputStream();
    295         }
    296 
    297 }