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 }