| /* |
| * Copyright 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.basicsyncadapter; |
| |
| import android.accounts.Account; |
| import android.annotation.TargetApi; |
| import android.content.AbstractThreadedSyncAdapter; |
| import android.content.ContentProviderClient; |
| import android.content.ContentProviderOperation; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.OperationApplicationException; |
| import android.content.SyncResult; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import com.example.android.basicsyncadapter.net.FeedParser; |
| import com.example.android.basicsyncadapter.provider.FeedContract; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.HttpURLConnection; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * Define a sync adapter for the app. |
| * |
| * <p>This class is instantiated in {@link SyncService}, which also binds SyncAdapter to the system. |
| * SyncAdapter should only be initialized in SyncService, never anywhere else. |
| * |
| * <p>The system calls onPerformSync() via an RPC call through the IBinder object supplied by |
| * SyncService. |
| */ |
| class SyncAdapter extends AbstractThreadedSyncAdapter { |
| public static final String TAG = "SyncAdapter"; |
| |
| /** |
| * URL to fetch content from during a sync. |
| * |
| * <p>This points to the Android Developers Blog. (Side note: We highly recommend reading the |
| * Android Developer Blog to stay up to date on the latest Android platform developments!) |
| */ |
| private static final String FEED_URL = "http://android-developers.blogspot.com/atom.xml"; |
| |
| /** |
| * Network connection timeout, in milliseconds. |
| */ |
| private static final int NET_CONNECT_TIMEOUT_MILLIS = 15000; // 15 seconds |
| |
| /** |
| * Network read timeout, in milliseconds. |
| */ |
| private static final int NET_READ_TIMEOUT_MILLIS = 10000; // 10 seconds |
| |
| /** |
| * Content resolver, for performing database operations. |
| */ |
| private final ContentResolver mContentResolver; |
| |
| /** |
| * Project used when querying content provider. Returns all known fields. |
| */ |
| private static final String[] PROJECTION = new String[] { |
| FeedContract.Entry._ID, |
| FeedContract.Entry.COLUMN_NAME_ENTRY_ID, |
| FeedContract.Entry.COLUMN_NAME_TITLE, |
| FeedContract.Entry.COLUMN_NAME_LINK, |
| FeedContract.Entry.COLUMN_NAME_PUBLISHED}; |
| |
| // Constants representing column positions from PROJECTION. |
| public static final int COLUMN_ID = 0; |
| public static final int COLUMN_ENTRY_ID = 1; |
| public static final int COLUMN_TITLE = 2; |
| public static final int COLUMN_LINK = 3; |
| public static final int COLUMN_PUBLISHED = 4; |
| |
| /** |
| * Constructor. Obtains handle to content resolver for later use. |
| */ |
| public SyncAdapter(Context context, boolean autoInitialize) { |
| super(context, autoInitialize); |
| mContentResolver = context.getContentResolver(); |
| } |
| |
| /** |
| * Constructor. Obtains handle to content resolver for later use. |
| */ |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { |
| super(context, autoInitialize, allowParallelSyncs); |
| mContentResolver = context.getContentResolver(); |
| } |
| |
| /** |
| * Called by the Android system in response to a request to run the sync adapter. The work |
| * required to read data from the network, parse it, and store it in the content provider is |
| * done here. Extending AbstractThreadedSyncAdapter ensures that all methods within SyncAdapter |
| * run on a background thread. For this reason, blocking I/O and other long-running tasks can be |
| * run <em>in situ</em>, and you don't have to set up a separate thread for them. |
| . |
| * |
| * <p>This is where we actually perform any work required to perform a sync. |
| * {@link android.content.AbstractThreadedSyncAdapter} guarantees that this will be called on a non-UI thread, |
| * so it is safe to peform blocking I/O here. |
| * |
| * <p>The syncResult argument allows you to pass information back to the method that triggered |
| * the sync. |
| */ |
| @Override |
| public void onPerformSync(Account account, Bundle extras, String authority, |
| ContentProviderClient provider, SyncResult syncResult) { |
| Log.i(TAG, "Beginning network synchronization"); |
| try { |
| final URL location = new URL(FEED_URL); |
| InputStream stream = null; |
| |
| try { |
| Log.i(TAG, "Streaming data from network: " + location); |
| stream = downloadUrl(location); |
| updateLocalFeedData(stream, syncResult); |
| // Makes sure that the InputStream is closed after the app is |
| // finished using it. |
| } finally { |
| if (stream != null) { |
| stream.close(); |
| } |
| } |
| } catch (MalformedURLException e) { |
| Log.e(TAG, "Feed URL is malformed", e); |
| syncResult.stats.numParseExceptions++; |
| return; |
| } catch (IOException e) { |
| Log.e(TAG, "Error reading from network: " + e.toString()); |
| syncResult.stats.numIoExceptions++; |
| return; |
| } catch (XmlPullParserException e) { |
| Log.e(TAG, "Error parsing feed: " + e.toString()); |
| syncResult.stats.numParseExceptions++; |
| return; |
| } catch (ParseException e) { |
| Log.e(TAG, "Error parsing feed: " + e.toString()); |
| syncResult.stats.numParseExceptions++; |
| return; |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error updating database: " + e.toString()); |
| syncResult.databaseError = true; |
| return; |
| } catch (OperationApplicationException e) { |
| Log.e(TAG, "Error updating database: " + e.toString()); |
| syncResult.databaseError = true; |
| return; |
| } |
| Log.i(TAG, "Network synchronization complete"); |
| } |
| |
| /** |
| * Read XML from an input stream, storing it into the content provider. |
| * |
| * <p>This is where incoming data is persisted, committing the results of a sync. In order to |
| * minimize (expensive) disk operations, we compare incoming data with what's already in our |
| * database, and compute a merge. Only changes (insert/update/delete) will result in a database |
| * write. |
| * |
| * <p>As an additional optimization, we use a batch operation to perform all database writes at |
| * once. |
| * |
| * <p>Merge strategy: |
| * 1. Get cursor to all items in feed<br/> |
| * 2. For each item, check if it's in the incoming data.<br/> |
| * a. YES: Remove from "incoming" list. Check if data has mutated, if so, perform |
| * database UPDATE.<br/> |
| * b. NO: Schedule DELETE from database.<br/> |
| * (At this point, incoming database only contains missing items.)<br/> |
| * 3. For any items remaining in incoming list, ADD to database. |
| */ |
| public void updateLocalFeedData(final InputStream stream, final SyncResult syncResult) |
| throws IOException, XmlPullParserException, RemoteException, |
| OperationApplicationException, ParseException { |
| final FeedParser feedParser = new FeedParser(); |
| final ContentResolver contentResolver = getContext().getContentResolver(); |
| |
| Log.i(TAG, "Parsing stream as Atom feed"); |
| final List<FeedParser.Entry> entries = feedParser.parse(stream); |
| Log.i(TAG, "Parsing complete. Found " + entries.size() + " entries"); |
| |
| |
| ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); |
| |
| // Build hash table of incoming entries |
| HashMap<String, FeedParser.Entry> entryMap = new HashMap<String, FeedParser.Entry>(); |
| for (FeedParser.Entry e : entries) { |
| entryMap.put(e.id, e); |
| } |
| |
| // Get list of all items |
| Log.i(TAG, "Fetching local entries for merge"); |
| Uri uri = FeedContract.Entry.CONTENT_URI; // Get all entries |
| Cursor c = contentResolver.query(uri, PROJECTION, null, null, null); |
| assert c != null; |
| Log.i(TAG, "Found " + c.getCount() + " local entries. Computing merge solution..."); |
| |
| // Find stale data |
| int id; |
| String entryId; |
| String title; |
| String link; |
| long published; |
| while (c.moveToNext()) { |
| syncResult.stats.numEntries++; |
| id = c.getInt(COLUMN_ID); |
| entryId = c.getString(COLUMN_ENTRY_ID); |
| title = c.getString(COLUMN_TITLE); |
| link = c.getString(COLUMN_LINK); |
| published = c.getLong(COLUMN_PUBLISHED); |
| FeedParser.Entry match = entryMap.get(entryId); |
| if (match != null) { |
| // Entry exists. Remove from entry map to prevent insert later. |
| entryMap.remove(entryId); |
| // Check to see if the entry needs to be updated |
| Uri existingUri = FeedContract.Entry.CONTENT_URI.buildUpon() |
| .appendPath(Integer.toString(id)).build(); |
| if ((match.title != null && !match.title.equals(title)) || |
| (match.link != null && !match.link.equals(link)) || |
| (match.published != published)) { |
| // Update existing record |
| Log.i(TAG, "Scheduling update: " + existingUri); |
| batch.add(ContentProviderOperation.newUpdate(existingUri) |
| .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, title) |
| .withValue(FeedContract.Entry.COLUMN_NAME_LINK, link) |
| .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, published) |
| .build()); |
| syncResult.stats.numUpdates++; |
| } else { |
| Log.i(TAG, "No action: " + existingUri); |
| } |
| } else { |
| // Entry doesn't exist. Remove it from the database. |
| Uri deleteUri = FeedContract.Entry.CONTENT_URI.buildUpon() |
| .appendPath(Integer.toString(id)).build(); |
| Log.i(TAG, "Scheduling delete: " + deleteUri); |
| batch.add(ContentProviderOperation.newDelete(deleteUri).build()); |
| syncResult.stats.numDeletes++; |
| } |
| } |
| c.close(); |
| |
| // Add new items |
| for (FeedParser.Entry e : entryMap.values()) { |
| Log.i(TAG, "Scheduling insert: entry_id=" + e.id); |
| batch.add(ContentProviderOperation.newInsert(FeedContract.Entry.CONTENT_URI) |
| .withValue(FeedContract.Entry.COLUMN_NAME_ENTRY_ID, e.id) |
| .withValue(FeedContract.Entry.COLUMN_NAME_TITLE, e.title) |
| .withValue(FeedContract.Entry.COLUMN_NAME_LINK, e.link) |
| .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, e.published) |
| .build()); |
| syncResult.stats.numInserts++; |
| } |
| Log.i(TAG, "Merge solution ready. Applying batch update"); |
| mContentResolver.applyBatch(FeedContract.CONTENT_AUTHORITY, batch); |
| mContentResolver.notifyChange( |
| FeedContract.Entry.CONTENT_URI, // URI where data was modified |
| null, // No local observer |
| false); // IMPORTANT: Do not sync to network |
| // This sample doesn't support uploads, but if *your* code does, make sure you set |
| // syncToNetwork=false in the line above to prevent duplicate syncs. |
| } |
| |
| /** |
| * Given a string representation of a URL, sets up a connection and gets an input stream. |
| */ |
| private InputStream downloadUrl(final URL url) throws IOException { |
| HttpURLConnection conn = (HttpURLConnection) url.openConnection(); |
| conn.setReadTimeout(NET_READ_TIMEOUT_MILLIS /* milliseconds */); |
| conn.setConnectTimeout(NET_CONNECT_TIMEOUT_MILLIS /* milliseconds */); |
| conn.setRequestMethod("GET"); |
| conn.setDoInput(true); |
| // Starts the query |
| conn.connect(); |
| return conn.getInputStream(); |
| } |
| } |