/* * 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. * *
This class is instantiated in {@link SyncService}, which also binds SyncAdapter to the system. * SyncAdapter should only be initialized in SyncService, never anywhere else. * *
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. * *
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 in situ, and you don't have to set up a separate thread for them. . * *
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. * *
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. * *
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. * *
As an additional optimization, we use a batch operation to perform all database writes at * once. * *
Merge strategy:
* 1. Get cursor to all items in feed
* 2. For each item, check if it's in the incoming data.
* a. YES: Remove from "incoming" list. Check if data has mutated, if so, perform
* database UPDATE.
* b. NO: Schedule DELETE from database.
* (At this point, incoming database only contains missing items.)
* 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