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