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