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