/*
* Copyright 2012 Google Inc.
*
* 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.android.apps.iosched.io;
import com.google.android.apps.iosched.R;
import com.google.android.apps.iosched.io.model.Event;
import com.google.android.apps.iosched.io.model.SessionsResponse;
import com.google.android.apps.iosched.io.model.SessionsResult;
import com.google.android.apps.iosched.provider.ScheduleContract;
import com.google.android.apps.iosched.provider.ScheduleContract.Sessions;
import com.google.android.apps.iosched.provider.ScheduleContract.SyncColumns;
import com.google.android.apps.iosched.util.Lists;
import com.google.android.apps.iosched.util.ParserUtils;
import com.google.gson.Gson;
import android.content.ContentProviderOperation;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.text.format.Time;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import static com.google.android.apps.iosched.provider.ScheduleDatabase.SessionsSpeakers;
import static com.google.android.apps.iosched.provider.ScheduleDatabase.SessionsTracks;
import static com.google.android.apps.iosched.util.LogUtils.LOGI;
import static com.google.android.apps.iosched.util.LogUtils.LOGW;
import static com.google.android.apps.iosched.util.LogUtils.makeLogTag;
import static com.google.android.apps.iosched.util.ParserUtils.sanitizeId;
/**
* Handler that parses session JSON data into a list of content provider operations.
*/
public class SessionsHandler extends JSONHandler {
private static final String TAG = makeLogTag(SessionsHandler.class);
private static final String BASE_SESSION_URL
= "https://developers.google.com/events/io/sessions/";
private static final String EVENT_TYPE_KEYNOTE = "keynote";
private static final String EVENT_TYPE_CODELAB = "codelab";
private static final int PARSE_FLAG_FORCE_SCHEDULE_REMOVE = 1;
private static final int PARSE_FLAG_FORCE_SCHEDULE_ADD = 2;
private static final Time sTime = new Time();
private static final Pattern sRemoveSpeakerIdPrefixPattern = Pattern.compile(".*//");
private boolean mLocal;
private boolean mThrowIfNoAuthToken;
public SessionsHandler(Context context, boolean local, boolean throwIfNoAuthToken) {
super(context);
mLocal = local;
mThrowIfNoAuthToken = throwIfNoAuthToken;
}
public ArrayList<ContentProviderOperation> parse(String json)
throws IOException {
final ArrayList<ContentProviderOperation> batch = Lists.newArrayList();
SessionsResponse response = new Gson().fromJson(json, SessionsResponse.class);
int numEvents = 0;
if (response.result != null) {
numEvents = response.result[0].events.length;
}
if (numEvents > 0) {
LOGI(TAG, "Updating sessions data");
// by default retain locally starred if local sync
boolean retainLocallyStarredSessions = mLocal;
if (response.error != null && response.error.isJsonPrimitive()) {
String errorMessageLower = response.error.getAsString().toLowerCase();
if (!mLocal && (errorMessageLower.contains("no profile")
|| errorMessageLower.contains("no auth token"))) {
// There was some authentication issue; retain locally starred sessions.
retainLocallyStarredSessions = true;
LOGW(TAG, "The user has no developers.google.com profile or this call is "
+ "not authenticated. Retaining locally starred sessions.");
}
if (mThrowIfNoAuthToken && errorMessageLower.contains("no auth token")) {
throw new HandlerException.UnauthorizedException("No auth token but we tried "
+ "authenticating. Need to invalidate the auth token.");
}
}
Set<String> starredSessionIds = new HashSet<String>();
if (retainLocallyStarredSessions) {
// Collect the list of current starred sessions
Cursor starredSessionsCursor = mContext.getContentResolver().query(
Sessions.CONTENT_STARRED_URI,
new String[]{ScheduleContract.Sessions.SESSION_ID},
null, null, null);
while (starredSessionsCursor.moveToNext()) {
starredSessionIds.add(starredSessionsCursor.getString(0));
}
starredSessionsCursor.close();
}
// Clear out existing sessions
batch.add(ContentProviderOperation
.newDelete(ScheduleContract.addCallerIsSyncAdapterParameter(
Sessions.CONTENT_URI))
.build());
// Maintain a list of created block IDs
Set<String> blockIds = new HashSet<String>();
for (SessionsResult result : response.result) {
for (Event event : result.events) {
int flags = 0;
if (retainLocallyStarredSessions) {
flags = (starredSessionIds.contains(event.id)
? PARSE_FLAG_FORCE_SCHEDULE_ADD
: PARSE_FLAG_FORCE_SCHEDULE_REMOVE);
}
String sessionId = event.id;
if (TextUtils.isEmpty(sessionId)) {
LOGW(TAG, "Found session with empty ID in API response.");
continue;
}
// Session title
String sessionTitle = event.title;
if (EVENT_TYPE_CODELAB.equals(result.event_type)) {
sessionTitle = mContext.getString(
R.string.codelab_title_template, sessionTitle);
}
// Whether or not it's in the schedule
boolean inSchedule = "Y".equals(event.attending);
if ((flags & PARSE_FLAG_FORCE_SCHEDULE_ADD) != 0
|| (flags & PARSE_FLAG_FORCE_SCHEDULE_REMOVE) != 0) {
inSchedule = (flags & PARSE_FLAG_FORCE_SCHEDULE_ADD) != 0;
}
if (EVENT_TYPE_KEYNOTE.equals(result.event_type)) {
// Keynotes are always in your schedule.
inSchedule = true;
}
// Re-order session tracks so that Code Lab is last
if (event.track != null) {
Arrays.sort(event.track, sTracksComparator);
}
// Hashtags
String hashtags = "";
if (event.track != null) {
StringBuilder hashtagsBuilder = new StringBuilder();
for (String trackName : event.track) {
if (trackName.trim().startsWith("Code Lab")) {
trackName = "Code Labs";
}
if (trackName.contains("Keynote")) {
continue;
}
hashtagsBuilder.append(" #");
hashtagsBuilder.append(
ScheduleContract.Tracks.generateTrackId(trackName));
}
hashtags = hashtagsBuilder.toString().trim();
}
// Pre-reqs
String prereqs = "";
if (event.prereq != null && event.prereq.length > 0) {
StringBuilder sb = new StringBuilder();
for (String prereq : event.prereq) {
sb.append(prereq);
sb.append(" ");
}
prereqs = sb.toString();
if (prereqs.startsWith("<br>")) {
prereqs = prereqs.substring(4);
}
}
String youtubeUrl = null;
if (event.youtube_url != null && event.youtube_url.length > 0) {
youtubeUrl = event.youtube_url[0];
}
// Insert session info
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newInsert(ScheduleContract
.addCallerIsSyncAdapterParameter(Sessions.CONTENT_URI))
.withValue(SyncColumns.UPDATED, System.currentTimeMillis())
.withValue(Sessions.SESSION_ID, sessionId)
.withValue(Sessions.SESSION_TYPE, result.event_type)
.withValue(Sessions.SESSION_LEVEL, event.level)
.withValue(Sessions.SESSION_TITLE, sessionTitle)
.withValue(Sessions.SESSION_ABSTRACT, event._abstract)
.withValue(Sessions.SESSION_TAGS, event.tags)
.withValue(Sessions.SESSION_URL, makeSessionUrl(sessionId))
.withValue(Sessions.SESSION_LIVESTREAM_URL, event.livestream_url)
.withValue(Sessions.SESSION_REQUIREMENTS, prereqs)
.withValue(Sessions.SESSION_STARRED, inSchedule)
.withValue(Sessions.SESSION_HASHTAGS, hashtags)
.withValue(Sessions.SESSION_YOUTUBE_URL, youtubeUrl)
.withValue(Sessions.SESSION_PDF_URL, "")
.withValue(Sessions.SESSION_NOTES_URL, "")
.withValue(Sessions.ROOM_ID, sanitizeId(event.room));
long sessionStartTime = parseTime(event.start_date, event.start_time);
long sessionEndTime = parseTime(event.end_date, event.end_time);
String blockId = ScheduleContract.Blocks.generateBlockId(
sessionStartTime, sessionEndTime);
if (!blockIds.contains(blockId)) {
String blockType;
String blockTitle;
if (EVENT_TYPE_KEYNOTE.equals(result.event_type)) {
blockType = ParserUtils.BLOCK_TYPE_KEYNOTE;
blockTitle = mContext.getString(R.string.schedule_block_title_keynote);
} else if (EVENT_TYPE_CODELAB.equals(result.event_type)) {
blockType = ParserUtils.BLOCK_TYPE_CODE_LAB;
blockTitle = mContext
.getString(R.string.schedule_block_title_code_labs);
} else {
blockType = ParserUtils.BLOCK_TYPE_SESSION;
blockTitle = mContext.getString(R.string.schedule_block_title_sessions);
}
batch.add(ContentProviderOperation
.newInsert(ScheduleContract.Blocks.CONTENT_URI)
.withValue(ScheduleContract.Blocks.BLOCK_ID, blockId)
.withValue(ScheduleContract.Blocks.BLOCK_TYPE, blockType)
.withValue(ScheduleContract.Blocks.BLOCK_TITLE, blockTitle)
.withValue(ScheduleContract.Blocks.BLOCK_START, sessionStartTime)
.withValue(ScheduleContract.Blocks.BLOCK_END, sessionEndTime)
.build());
blockIds.add(blockId);
}
builder.withValue(Sessions.BLOCK_ID, blockId);
batch.add(builder.build());
// Replace all session speakers
final Uri sessionSpeakersUri = Sessions.buildSpeakersDirUri(sessionId);
batch.add(ContentProviderOperation
.newDelete(ScheduleContract
.addCallerIsSyncAdapterParameter(sessionSpeakersUri))
.build());
if (event.speaker_id != null) {
for (String speakerId : event.speaker_id) {
speakerId = sRemoveSpeakerIdPrefixPattern.matcher(speakerId).replaceAll(
"");
batch.add(ContentProviderOperation.newInsert(sessionSpeakersUri)
.withValue(SessionsSpeakers.SESSION_ID, sessionId)
.withValue(SessionsSpeakers.SPEAKER_ID, speakerId).build());
}
}
// Replace all session tracks
final Uri sessionTracksUri = ScheduleContract.addCallerIsSyncAdapterParameter(
Sessions.buildTracksDirUri(sessionId));
batch.add(ContentProviderOperation.newDelete(sessionTracksUri).build());
if (event.track != null) {
for (String trackName : event.track) {
if (trackName.contains("Code Lab")) {
trackName = "Code Labs";
}
String trackId = ScheduleContract.Tracks.generateTrackId(trackName);
batch.add(ContentProviderOperation.newInsert(sessionTracksUri)
.withValue(SessionsTracks.SESSION_ID, sessionId)
.withValue(SessionsTracks.TRACK_ID, trackId).build());
}
}
}
}
}
return batch;
}
private Comparator<String> sTracksComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
// TODO: improve performance of this comparator
return (s1.contains("Code Lab") ? "z" : s1).compareTo(
(s2.contains("Code Lab") ? "z" : s2));
}
};
private String makeSessionUrl(String sessionId) {
if (TextUtils.isEmpty(sessionId)) {
return null;
}
return BASE_SESSION_URL + sessionId;
}
private static long parseTime(String date, String time) {
//change to this format : 2011-05-10T07:00:00.000-07:00
int index = time.indexOf(":");
if (index == 1) {
time = "0" + time;
}
final String composed = String.format("%sT%s:00.000-07:00", date, time);
//return sTimeFormat.parse(composed).getTime();
sTime.parse3339(composed);
return sTime.toMillis(false);
}
}