/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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.google.samples.apps.iosched.sync;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import com.google.samples.apps.iosched.io.*;
import com.google.samples.apps.iosched.io.map.model.Tile;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.util.FileUtils;
import com.google.samples.apps.iosched.util.Lists;
import com.google.samples.apps.iosched.util.MapUtils;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import java.io.*;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import com.larvalabs.svgandroid.SVG;
import com.larvalabs.svgandroid.SVGBuilder;
import com.larvalabs.svgandroid.SVGParseException;
import com.turbomanage.httpclient.BasicHttpClient;
import com.turbomanage.httpclient.ConsoleRequestLogger;
import com.turbomanage.httpclient.HttpResponse;
import com.turbomanage.httpclient.RequestLogger;
import static com.google.samples.apps.iosched.util.LogUtils.*;
/**
* Helper class that parses conference data and imports them into the app's
* Content Provider.
*/
public class ConferenceDataHandler {
private static final String TAG = makeLogTag(SyncHelper.class);
// Shared preferences key under which we store the timestamp that corresponds to
// the data we currently have in our content provider.
private static final String SP_KEY_DATA_TIMESTAMP = "data_timestamp";
// symbolic timestamp to use when we are missing timestamp data (which means our data is
// really old or nonexistent)
private static final String DEFAULT_TIMESTAMP = "Sat, 1 Jan 2000 00:00:00 GMT";
private static final String DATA_KEY_ROOMS = "rooms";
private static final String DATA_KEY_BLOCKS = "blocks";
private static final String DATA_KEY_TAGS = "tags";
private static final String DATA_KEY_SPEAKERS = "speakers";
private static final String DATA_KEY_SESSIONS = "sessions";
private static final String DATA_KEY_SEARCH_SUGGESTIONS = "search_suggestions";
private static final String DATA_KEY_MAP = "map";
private static final String DATA_KEY_HASHTAGS = "hashtags";
private static final String DATA_KEY_EXPERTS = "experts";
private static final String DATA_KEY_VIDEOS = "video_library";
private static final String DATA_KEY_PARTNERS = "partners";
private static final String[] DATA_KEYS_IN_ORDER = {
DATA_KEY_ROOMS,
DATA_KEY_BLOCKS,
DATA_KEY_TAGS,
DATA_KEY_SPEAKERS,
DATA_KEY_SESSIONS,
DATA_KEY_SEARCH_SUGGESTIONS,
DATA_KEY_MAP,
DATA_KEY_HASHTAGS,
DATA_KEY_EXPERTS,
DATA_KEY_VIDEOS,
DATA_KEY_PARTNERS
};
Context mContext = null;
// Handlers for each entity type:
RoomsHandler mRoomsHandler = null;
BlocksHandler mBlocksHandler = null;
TagsHandler mTagsHandler = null;
SpeakersHandler mSpeakersHandler = null;
SessionsHandler mSessionsHandler = null;
SearchSuggestHandler mSearchSuggestHandler = null;
MapPropertyHandler mMapPropertyHandler = null;
ExpertsHandler mExpertsHandler = null;
HashtagsHandler mHashtagsHandler = null;
VideosHandler mVideosHandler = null;
PartnersHandler mPartnersHandler = null;
// Convenience map that maps the key name to its corresponding handler (e.g.
// "blocks" to mBlocksHandler (to avoid very tedious if-elses)
HashMap<String, JSONHandler> mHandlerForKey = new HashMap<String, JSONHandler>();
// Tally of total content provider operations we carried out (for statistical purposes)
private int mContentProviderOperationsDone = 0;
public ConferenceDataHandler(Context ctx) {
mContext = ctx;
}
/**
* Parses the conference data in the given objects and imports the data into the
* content provider. The format of the data is documented at https://code.google.com/p/iosched.
*
* @param dataBodies The collection of JSON objects to parse and import.
* @param dataTimestamp The timestamp of the data. This should be in RFC1123 format.
* @param downloadsAllowed Whether or not we are supposed to download data from the internet if needed.
* @throws IOException If there is a problem parsing the data.
*/
public void applyConferenceData(String[] dataBodies, String dataTimestamp,
boolean downloadsAllowed) throws IOException {
LOGD(TAG, "Applying data from " + dataBodies.length + " files, timestamp " + dataTimestamp);
// create handlers for each data type
mHandlerForKey.put(DATA_KEY_ROOMS, mRoomsHandler = new RoomsHandler(mContext));
mHandlerForKey.put(DATA_KEY_BLOCKS, mBlocksHandler = new BlocksHandler(mContext));
mHandlerForKey.put(DATA_KEY_TAGS, mTagsHandler = new TagsHandler(mContext));
mHandlerForKey.put(DATA_KEY_SPEAKERS, mSpeakersHandler = new SpeakersHandler(mContext));
mHandlerForKey.put(DATA_KEY_SESSIONS, mSessionsHandler = new SessionsHandler(mContext));
mHandlerForKey.put(DATA_KEY_SEARCH_SUGGESTIONS, mSearchSuggestHandler =
new SearchSuggestHandler(mContext));
mHandlerForKey.put(DATA_KEY_MAP, mMapPropertyHandler = new MapPropertyHandler(mContext));
mHandlerForKey.put(DATA_KEY_EXPERTS, mExpertsHandler = new ExpertsHandler(mContext));
mHandlerForKey.put(DATA_KEY_HASHTAGS, mHashtagsHandler = new HashtagsHandler(mContext));
mHandlerForKey.put(DATA_KEY_VIDEOS, mVideosHandler = new VideosHandler(mContext));
mHandlerForKey.put(DATA_KEY_PARTNERS, mPartnersHandler = new PartnersHandler(mContext));
// process the jsons. This will call each of the handlers when appropriate to deal
// with the objects we see in the data.
LOGD(TAG, "Processing " + dataBodies.length + " JSON objects.");
for (int i = 0; i < dataBodies.length; i++) {
LOGD(TAG, "Processing json object #" + (i + 1) + " of " + dataBodies.length);
processDataBody(dataBodies[i]);
}
// the sessions handler needs to know the tag and speaker maps to process sessions
mSessionsHandler.setTagMap(mTagsHandler.getTagMap());
mSessionsHandler.setSpeakerMap(mSpeakersHandler.getSpeakerMap());
// produce the necessary content provider operations
ArrayList<ContentProviderOperation> batch = new ArrayList<ContentProviderOperation>();
for (String key : DATA_KEYS_IN_ORDER) {
LOGD(TAG, "Building content provider operations for: " + key);
mHandlerForKey.get(key).makeContentProviderOperations(batch);
LOGD(TAG, "Content provider operations so far: " + batch.size());
}
LOGD(TAG, "Total content provider operations: " + batch.size());
// download or process local map tile overlay files (SVG files)
LOGD(TAG, "Processing map overlay files");
processMapOverlayFiles(mMapPropertyHandler.getTileOverlays(), downloadsAllowed);
// finally, push the changes into the Content Provider
LOGD(TAG, "Applying " + batch.size() + " content provider operations.");
try {
int operations = batch.size();
if (operations > 0) {
mContext.getContentResolver().applyBatch(ScheduleContract.CONTENT_AUTHORITY, batch);
}
LOGD(TAG, "Successfully applied " + operations + " content provider operations.");
mContentProviderOperationsDone += operations;
} catch (RemoteException ex) {
LOGE(TAG, "RemoteException while applying content provider operations.");
throw new RuntimeException("Error executing content provider batch operation", ex);
} catch (OperationApplicationException ex) {
LOGE(TAG, "OperationApplicationException while applying content provider operations.");
throw new RuntimeException("Error executing content provider batch operation", ex);
}
// notify all top-level paths
LOGD(TAG, "Notifying changes on all top-level paths on Content Resolver.");
ContentResolver resolver = mContext.getContentResolver();
for (String path : ScheduleContract.TOP_LEVEL_PATHS) {
Uri uri = ScheduleContract.BASE_CONTENT_URI.buildUpon().appendPath(path).build();
resolver.notifyChange(uri, null);
}
// update our data timestamp
setDataTimestamp(dataTimestamp);
LOGD(TAG, "Done applying conference data.");
}
public int getContentProviderOperationsDone() {
return mContentProviderOperationsDone;
}
/**
* Processes a conference data body and calls the appropriate data type handlers
* to process each of the objects represented therein.
*
* @param dataBody The body of data to process
* @throws IOException If there is an error parsing the data.
*/
private void processDataBody(String dataBody) throws IOException {
JsonReader reader = new JsonReader(new StringReader(dataBody));
JsonParser parser = new JsonParser();
try {
reader.setLenient(true); // To err is human
// the whole file is a single JSON object
reader.beginObject();
while (reader.hasNext()) {
// the key is "rooms", "speakers", "tracks", etc.
String key = reader.nextName();
if (mHandlerForKey.containsKey(key)) {
// pass the value to the corresponding handler
mHandlerForKey.get(key).process(parser.parse(reader));
} else {
LOGW(TAG, "Skipping unknown key in conference data json: " + key);
reader.skipValue();
}
}
reader.endObject();
} finally {
reader.close();
}
}
/**
* Synchronise the map overlay files either from the local assets (if available) or from a remote url.
*
* @param collection Set of tiles containing a local filename and remote url.
* @throws IOException
*/
private void processMapOverlayFiles(Collection<Tile> collection, boolean downloadAllowed) throws IOException, SVGParseException {
// clear the tile cache on disk if any tiles have been updated
boolean shouldClearCache = false;
// keep track of used files, unused files are removed
ArrayList<String> usedTiles = Lists.newArrayList();
for (Tile tile : collection) {
final String filename = tile.filename;
final String url = tile.url;
usedTiles.add(filename);
if (!MapUtils.hasTile(mContext, filename)) {
shouldClearCache = true;
// copy or download the tile if it is not stored yet
if (MapUtils.hasTileAsset(mContext, filename)) {
// file already exists as an asset, copy it
MapUtils.copyTileAsset(mContext, filename);
} else if (downloadAllowed && !TextUtils.isEmpty(url)) {
try {
// download the file only if downloads are allowed and url is not empty
File tileFile = MapUtils.getTileFile(mContext, filename);
BasicHttpClient httpClient = new BasicHttpClient();
httpClient.setRequestLogger(mQuietLogger);
HttpResponse httpResponse = httpClient.get(url, null);
FileUtils.writeFile(httpResponse.getBody(), tileFile);
// ensure the file is valid SVG
InputStream is = new FileInputStream(tileFile);
SVG svg = new SVGBuilder().readFromInputStream(is).build();
is.close();
} catch (IOException ex) {
LOGE(TAG, "FAILED downloading map overlay tile "+url+
": " + ex.getMessage(), ex);
} catch (SVGParseException ex) {
LOGE(TAG, "FAILED parsing map overlay tile "+url+
": " + ex.getMessage(), ex);
}
} else {
LOGD(TAG, "Skipping download of map overlay tile" +
" (since downloadsAllowed=false)");
}
}
}
if (shouldClearCache) {
MapUtils.clearDiskCache(mContext);
}
MapUtils.removeUnusedTiles(mContext, usedTiles);
}
// Returns the timestamp of the data we have in the content provider.
public String getDataTimestamp() {
return PreferenceManager.getDefaultSharedPreferences(mContext).getString(
SP_KEY_DATA_TIMESTAMP, DEFAULT_TIMESTAMP);
}
// Sets the timestamp of the data we have in the content provider.
public void setDataTimestamp(String timestamp) {
LOGD(TAG, "Setting data timestamp to: " + timestamp);
PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(
SP_KEY_DATA_TIMESTAMP, timestamp).commit();
}
// Reset the timestamp of the data we have in the content provider
public static void resetDataTimestamp(final Context context) {
LOGD(TAG, "Resetting data timestamp to default (to invalidate our synced data)");
PreferenceManager.getDefaultSharedPreferences(context).edit().remove(
SP_KEY_DATA_TIMESTAMP).commit();
}
/**
* A type of ConsoleRequestLogger that does not log requests and responses.
*/
private RequestLogger mQuietLogger = new ConsoleRequestLogger(){
@Override
public void logRequest(HttpURLConnection uc, Object content) throws IOException { }
@Override
public void logResponse(HttpResponse res) { }
};
}