Please note that the contents of this offline web site may be out of date. To access the most recent documentation visit the online version .
Note that links that point to online resources are green in color and will open in a new window.
We would love it if you could give us feedback about this material by filling this form (You have to be online to fill it)
BasicSyncAdapter / src / com.example.android.basicsyncadapter /

SyncAdapter.java

       
        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
       
       
        }