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