Skip to content

Most visited

Recently visited

navigation
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, match.title)
254
                            .withValue(FeedContract.Entry.COLUMN_NAME_LINK, match.link)
255
                            .withValue(FeedContract.Entry.COLUMN_NAME_PUBLISHED, match.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
}
This site uses cookies to store your preferences for site-specific language and display options.

Hooray!

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a one-minute survey?
Help us improve Android tools and documentation.