Java tutorial
/* * 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.weimed.app.sync; 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.weimed.app.provider.NewsContract; import com.weimed.app.utils.JSONParser; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; 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 com.google.common.io.CharStreams; /** * 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. * */ private static final String NEWS_URL_BASE = "http://192.168.1.138:5984/news_articles"; private static final String NEWS_URL = NEWS_URL_BASE + "/_design/article/_view/index"; /** * 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[] { NewsContract.Entry._ID, NewsContract.Entry.COLUMN_ENTRY_ID, NewsContract.Entry.COLUMN_TITLE, NewsContract.Entry.COLUMN_CONTENT, NewsContract.Entry.COLUMN_PUBLISHER, NewsContract.Entry.COLUMN_PICURL, NewsContract.Entry.COLUMN_ORIGINALURL, NewsContract.Entry.COLUMN_CREATEDAT, NewsContract.Entry.COLUMN_UPDATEDAT, NewsContract.Entry.COLUMN_PUBLISHEDAT }; // 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_CONTENT = 3; public static final int COLUMN_PUBLISHER = 4; public static final int COLUMN_PICURL = 5; public static final int COLUMN_ORIGINALURL = 6; public static final int COLUMN_CREATEDAT = 7; public static final int COLUMN_UPDATEDAT = 8; public static final int COLUMN_PUBLISHEDAT = 9; /** * 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 site</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 preform 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(NEWS_URL); InputStream stream = null; try { Log.i(TAG, "Streaming data from network: " + location); stream = downloadUrl(location); updateLocalJSONData(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 (JSONException 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 JSON 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 updateLocalJSONData(final InputStream stream, final SyncResult syncResult) throws IOException, JSONException, RemoteException, OperationApplicationException, ParseException { final JSONParser JSONParser = new JSONParser(); final ContentResolver contentResolver = getContext().getContentResolver(); Log.i(TAG, "Parsing stream as JSON Array"); final JSONObject json = JSONParser.parseJSONObject(stream); Log.i(TAG, "Parsing complete. Found " + json.getInt("total_rows") + " entries"); ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>(); // Build hash table of incoming entries HashMap<String, JSONObject> entryMap = new HashMap<String, JSONObject>(); final JSONArray entries = json.getJSONArray("rows"); for (int i = 0; i < json.getInt("total_rows"); i++) { JSONObject e = entries.getJSONObject(i).getJSONObject("value"); entryMap.put(e.getString("_id"), e); } // Get list of all items Log.i(TAG, "Fetching local entries for merge"); Uri uri = NewsContract.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 content; String publisher; String picurl; String originalurl; String createdat; String updatedat; String publishedat; while (c.moveToNext()) { syncResult.stats.numEntries++; id = c.getInt(COLUMN_ID); entryId = c.getString(COLUMN_ENTRY_ID); title = c.getString(COLUMN_TITLE); content = c.getString(COLUMN_CONTENT); publisher = c.getString(COLUMN_PUBLISHER); picurl = c.getString(COLUMN_PICURL); originalurl = c.getString(COLUMN_ORIGINALURL); createdat = c.getString(COLUMN_CREATEDAT); updatedat = c.getString(COLUMN_UPDATEDAT); publishedat = c.getString(COLUMN_PUBLISHEDAT); JSONObject 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 // How to know update local or remote? updatedAt! which is newer, update another. // Uri existingUri = NewsContract.Entry.CONTENT_URI.buildUpon() // .appendPath(Integer.toString(id)).build(); // if ((match.getString("title") != null && !match.getString("title").equals(title)) || // (match.getString("content") != null && !match.getString("content").equals(content)) || // (match.getString("publisher") != null && !match.getString("publisher").equals(publisher)) || // (match.getString("picurl") != null && !match.getString("picurl").equals(picurl)) || // (match.getString("originalurl") != null && !match.getString("originalurl").equals(originalurl)) || // (match.getString("createdat") != null && !match.getString("createdat").equals(createdat)) || // (match.getString("updatedat") != null && !match.getString("updatedat").equals(updatedat)) || // (match.getString("publishedat") != null && !match.getString("publishedat").equals(publishedat)) // ) { // // Update existing record // Log.i(TAG, "Scheduling update: " + existingUri); // batch.add(ContentProviderOperation.newUpdate(existingUri) // .withValue(NewsContract.Entry.COLUMN_TITLE, title) // .withValue(NewsContract.Entry.COLUMN_CONTENT, content) // .withValue(NewsContract.Entry.COLUMN_PUBLISHER, publisher) // .withValue(NewsContract.Entry.COLUMN_PICURL, picurl) // .withValue(NewsContract.Entry.COLUMN_ORIGINALURL, originalurl) // .withValue(NewsContract.Entry.COLUMN_CREATEDAT, createdat) // .withValue(NewsContract.Entry.COLUMN_UPDATEDAT, updatedat) // .withValue(NewsContract.Entry.COLUMN_PUBLISHEDAT, publishedat) // .build()); // syncResult.stats.numUpdates++; // } else { // Log.i(TAG, "No action: " + existingUri); // } // } else { // Entry doesn't exist. Remove it from the database. Uri deleteUri = NewsContract.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 (JSONObject e : entryMap.values()) { Log.i(TAG, "Scheduling insert: entry_id=" + e.getString("_id")); batch.add(ContentProviderOperation.newInsert(NewsContract.Entry.CONTENT_URI) .withValue(NewsContract.Entry.COLUMN_ENTRY_ID, e.getString("_id")) .withValue(NewsContract.Entry.COLUMN_TITLE, e.getString("title")) .withValue(NewsContract.Entry.COLUMN_CONTENT, fetchTextFileToString(NEWS_URL_BASE + '/' + e.getString("_id") + "/content.md")) .withValue(NewsContract.Entry.COLUMN_PUBLISHER, e.getString("publisher")) .withValue(NewsContract.Entry.COLUMN_PICURL, e.has("pic_link") ? e.getString("pic_link") : null) .withValue(NewsContract.Entry.COLUMN_ORIGINALURL, e.getString("origin_link")) .withValue(NewsContract.Entry.COLUMN_CREATEDAT, e.getString("created_at")) .withValue(NewsContract.Entry.COLUMN_UPDATEDAT, e.getString("updated_at")) .withValue(NewsContract.Entry.COLUMN_PUBLISHEDAT, e.getString("publish_at")).build()); syncResult.stats.numInserts++; } Log.i(TAG, "Merge solution ready. Applying batch update"); mContentResolver.applyBatch(NewsContract.CONTENT_AUTHORITY, batch); mContentResolver.notifyChange(NewsContract.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(); } /** Parse an http response, returning a String. * * @param in A feed, as a stream. * @return A String. * @throws java.io.IOException on I/O error. */ public String parseString(InputStream in) throws IOException, ParseException { try { BufferedReader streamReader = new BufferedReader(new InputStreamReader(in, "UTF-8")); StringBuilder builder = new StringBuilder(); String inputStr; while ((inputStr = streamReader.readLine()) != null) builder.append(inputStr); return builder.toString(); } finally { in.close(); } } /** Download the text file from URL string, returning a String. * * @param strUrl, as a String. * @return A String. * @throws java.io.IOException on I/O error. */ public String fetchTextFileToString(String strUrl) { URL url = null; try { url = new URL(strUrl); } catch (MalformedURLException e) { } String ret = ""; try { ret = CharStreams.toString(new InputStreamReader(url.openStream(), "UTF-8")); } catch (IOException e) { } return ret; } }