package edu.mit.mitmobile2.events; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import edu.mit.mitmobile2.MobileWebApi; import edu.mit.mitmobile2.MobileWebApi.ServerResponseException; import edu.mit.mitmobile2.objs.EventCategoryItem; import edu.mit.mitmobile2.objs.EventDetailsItem; import edu.mit.mitmobile2.objs.SearchResults; import android.content.Context; import android.os.Handler; import android.widget.Toast; public class EventsModel { private static int TIME_ZONE_OFFSET = 5; private static HashMap<String, EventDetailsItem> sBriefEventsCache = new HashMap<String, EventDetailsItem>(); private static HashMap<String, EventDetailsItem> sFullEventsCache = new HashMap<String, EventDetailsItem>(); public static class EventType { private String mId; private String mLongName; private String mShortName; private boolean mHasCategories; public EventType(String id, String longName, String shortName, boolean hasCategories) { mId = id; mLongName = longName; mShortName = shortName; mHasCategories = hasCategories; } public String getTypeId() { return mId; } public String getLongName() { return mLongName; } public String getShortName() { return mShortName; } public boolean hasCategories() { return mHasCategories; } } private static ArrayList<EventType> sEventTypes; public static void fetchEventTypes(Context context, final Handler uiHandler) { if(sEventTypes != null) { // cache already populated MobileWebApi.sendSuccessMessage(uiHandler); } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> params = new HashMap<String, String>(); params.put("module", "calendar"); params.put("command", "extraTopLevels"); params.put("version", "2"); webApi.requestJSONArray(params, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray array) throws ServerResponseException, JSONException { sEventTypes = new ArrayList<EventType>(); sEventTypes.add(new EventType("Events", "Today's Events", "Events", false)); for(int i = 0; i < array.length(); i++) { JSONObject eventType = array.getJSONObject(i); // server may not yet be returning verion 2 of the api boolean hasCategories = false; if(eventType.has("hasCategories")) { hasCategories = eventType.getBoolean("hasCategories"); } sEventTypes.add(new EventType( eventType.getString("type"), eventType.getString("longName"), eventType.getString("shortName"), hasCategories )); } MobileWebApi.sendSuccessMessage(uiHandler); } }); } public static List<EventType> getEventTypes() { return sEventTypes; } public static boolean eventTypesLoaded() { return sEventTypes != null; } public static EventType getEventType(String eventTypeId) { for(EventType eventType : sEventTypes) { if(eventType.getTypeId().equals(eventTypeId)) { return eventType; } } return null; } public static void fetchDayEvents(final long unixtime, final EventType eventType, Context context, final Handler uiHandler) { if(getDayEvents(unixtime, eventType) != null) { MobileWebApi.sendSuccessMessage(uiHandler); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "day"); eventParameters.put("time", Long.toString(unixtime)); eventParameters.put("type", eventType.getTypeId()); webApi.requestJSONArray(eventParameters, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray jArray) { List<EventDetailsItem> events = parseDetailArray(jArray); putInDayEventsCache(unixtime, eventType, events); MobileWebApi.sendSuccessMessage(uiHandler); } }); } private static List<EventDetailsItem> parseDetailArray(JSONArray jArray) { ArrayList<EventDetailsItem> events = new ArrayList<EventDetailsItem>(); for(int i = 0; i < jArray.length(); i++) { try { JSONObject jItem = (JSONObject) jArray.get(i); events.add(parseDetailItem(jItem)); } catch (JSONException e) { e.printStackTrace(); throw new RuntimeException("Failed to parsed EventItem"); } } return events; } private static EventDetailsItem parseDetailItem(JSONObject jItem) { EventDetailsItem item = new EventDetailsItem(); try { item.id = jItem.getString("id"); item.title = jItem.getString("title"); item.start = jItem.getLong("start"); item.end = optLong(jItem, "end"); item.owner = optString(jItem, "owner"); item.shortloc = optString(jItem, "shortloc"); item.location = optString(jItem, "location"); item.status = optString(jItem, "status"); item.event = optString(jItem, "event"); item.cancelled = optString(jItem, "cancelled"); item.infophone = optString(jItem, "infophone"); item.infourl = optString(jItem, "infourl"); item.description = optString(jItem, "description"); JSONObject jCoords = jItem.optJSONObject("coordinate"); if (jCoords!=null) { item.coordinates = item.new Coord(); item.coordinates.lon = jCoords.getDouble("lon"); item.coordinates.lat = jCoords.getDouble("lat"); item.coordinates.description = jCoords.optString("description", null); } } catch (JSONException e) { e.printStackTrace(); throw new RuntimeException("Failed to parsed EventItem"); } return item; } @SuppressWarnings("serial") private static class DayEventsIdsCache extends HashMap<Long, List<String>> {}; private static HashMap<String, DayEventsIdsCache> sEventsIdCache = new HashMap<String, DayEventsIdsCache>(); private static List<EventDetailsItem> eventList(List<String> eventIds) { ArrayList<EventDetailsItem> events = new ArrayList<EventDetailsItem>(); for(String eventId : eventIds) { events.add(sBriefEventsCache.get(eventId)); } return events; } private static List<String> eventIds(List<EventDetailsItem> events) { ArrayList<String> eventIds = new ArrayList<String>(); for(EventDetailsItem event : events) { sBriefEventsCache.put(event.id, event); eventIds.add(event.id); } return eventIds; } private static void putInDayEventsCache(long unixtime, EventType eventType, List<EventDetailsItem> events) { DayEventsIdsCache cache = getDayEventsCache(eventType); Long timeKey = getDayEventKey(unixtime); List<String> eventIds = eventIds(events); cache.put(new Long(timeKey), eventIds); } public static List<EventDetailsItem> getDayEvents(long unixtime, EventType eventType) { DayEventsIdsCache cache = getDayEventsCache(eventType); Long timeKey = getDayEventKey(unixtime); List<String> eventIds = cache.get(new Long(timeKey)); if(eventIds == null) { return null; } return eventList(eventIds); } private static DayEventsIdsCache getDayEventsCache(EventType eventType) { DayEventsIdsCache cache = sEventsIdCache.get(eventType.getTypeId()); if(cache == null) { cache = new DayEventsIdsCache(); sEventsIdCache.put(eventType.getTypeId(), cache); } return cache; } private static Long getDayEventKey(long unixtime) { // map all unixtimes for the same day to the same key unixtime -= TIME_ZONE_OFFSET * 60 * 60; long dayUnixtime = unixtime / (24 * 60 * 60); dayUnixtime += 12 * 60 * 60; return new Long(dayUnixtime); } /* * Event category related stuff */ private static HashMap<String, List<EventCategoryItem>> sCategories = new HashMap<String, List<EventCategoryItem>>(); public static boolean categoriesAvailable() { return (getEventType("Events") != null); } public static void fetchCategories(Context context, Handler uiHandler) { fetchCategories(context, getEventType("Events"), uiHandler); } public static void fetchCategories(Context context, final EventType type, final Handler uiHandler) { if(sCategories.containsKey(type.getTypeId())) { MobileWebApi.sendSuccessMessage(uiHandler); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("command", "categories"); eventParameters.put("module", "calendar"); eventParameters.put("type", type.getTypeId()); webApi.requestJSONArray(eventParameters, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray jArray) { sCategories.put(type.getTypeId(), parseCategoryArray(jArray)); MobileWebApi.sendSuccessMessage(uiHandler); } }); } public static List<EventCategoryItem> getCategories(EventType type) { return sCategories.get(type.getTypeId()); } public static List<EventCategoryItem> getCategories() { return sCategories.get("Events"); } public static EventCategoryItem getCategory(int categoryId) { for(EventCategoryItem categoryItem : getCategories()) { if(categoryItem.catid == categoryId) { return categoryItem; } } return null; } private static List<EventCategoryItem> parseCategoryArray(JSONArray jArray) { ArrayList<EventCategoryItem> categories = new ArrayList<EventCategoryItem>(); for(int i = 0; i < jArray.length(); i++) { try { JSONObject jItem = (JSONObject) jArray.get(i); categories.add(parseCategoryItem(jItem)); } catch (JSONException e) { e.printStackTrace(); throw new RuntimeException("Failed to parsed Event Categories"); } } return categories; } private static EventCategoryItem parseCategoryItem(JSONObject jItem) { EventCategoryItem category = new EventCategoryItem(); try { category.name = jItem.getString("name"); category.catid = jItem.getInt("catid"); JSONArray subcategories = jItem.optJSONArray("subcategories"); if(subcategories != null) { category.subcats = parseCategoryArray(subcategories); } } catch (JSONException e) { e.printStackTrace(); throw new RuntimeException("Failed to parsed Event Category"); } return category; } public static void fetchCategoryDayEvents(final long unixtime, final int categoryId, final EventType eventType, Context context, final Handler uiHandler) { if(getCategoryDayEvents(unixtime, categoryId, eventType) != null) { MobileWebApi.sendSuccessMessage(uiHandler); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "category"); eventParameters.put("type", eventType.getTypeId()); eventParameters.put("id", Integer.toString(categoryId)); eventParameters.put("start", Long.toString(unixtime)); webApi.requestJSONArray(eventParameters, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray jArray) { List<EventDetailsItem> events = parseDetailArray(jArray); putInCategoryDayEventsCache(unixtime, categoryId, eventType, events); MobileWebApi.sendSuccessMessage(uiHandler); } }); } // HashMap with keys of categoryId then day timestamp as a key // the values are lists of eventIds, this data structure is a little ugly static private HashMap<String, HashMap<Long, List<String>>> sCategoryDayEventsCache = new HashMap<String, HashMap<Long, List<String>>>(); private static String categoryKey(int categoryId, EventType eventType) { return eventType.getTypeId() + "-" + categoryId; } private static void putInCategoryDayEventsCache(long unixtime, int categoryId, EventType eventType, List<EventDetailsItem> events) { Long timeKey = getDayEventKey(unixtime); List<String> eventIds = eventIds(events); String categoryKey = categoryKey(categoryId, eventType); if(!sCategoryDayEventsCache.containsKey(categoryKey)) { sCategoryDayEventsCache.put(categoryKey, new HashMap<Long, List<String>>()); } HashMap<Long, List<String>> categoryCache = sCategoryDayEventsCache.get(categoryKey); categoryCache.put(timeKey, eventIds); } public static List<EventDetailsItem> getCategoryDayEvents(long unixtime, int categoryId, EventType eventType) { Long timeKey = getDayEventKey(unixtime); String categoryKey = categoryKey(categoryId, eventType); if(!sCategoryDayEventsCache.containsKey(categoryKey)) { return null; } if(!sCategoryDayEventsCache.get(categoryKey).containsKey(timeKey)) { return null; } List<String> eventIds = sCategoryDayEventsCache.get(categoryKey).get(timeKey); return eventList(eventIds); } /* * Search API call */ static private HashMap<String, List<String>> sSearchCache = new HashMap<String, List<String>>(); public static void executeSearch(final String searchTerms, final Context context, final Handler uiHandler) { if(sSearchCache.containsKey(searchTerms)) { List<EventDetailsItem> events = executeLocalSearch(searchTerms); MobileWebApi.sendSuccessMessage(uiHandler, new SearchResults<EventDetailsItem>(searchTerms, events)); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "search"); eventParameters.put("q", searchTerms); webApi.requestJSONObject(eventParameters, new MobileWebApi.JSONObjectResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONObject jObject) { try { JSONArray jArray; jArray = jObject.getJSONArray("events"); ArrayList<EventDetailsItem> events = new ArrayList<EventDetailsItem>(); events.addAll(parseDetailArray(jArray)); Collections.sort(events, sCompareEventTimes); sSearchCache.put(searchTerms, eventIds(events)); MobileWebApi.sendSuccessMessage(uiHandler, new SearchResults<EventDetailsItem>(searchTerms, events)); } catch (JSONException e) { e.printStackTrace(); Toast.makeText(context, "Failure parsing events search", Toast.LENGTH_SHORT).show(); MobileWebApi.sendErrorMessage(uiHandler); } } }); } private static Comparator<EventDetailsItem> sCompareEventTimes = new Comparator<EventDetailsItem>() { @Override public int compare(EventDetailsItem event1, EventDetailsItem event2) { Long time1 = event1.start; Long time2 = event2.start; return time1.compareTo(time2); } }; public static List<EventDetailsItem> executeLocalSearch(String searchTerms) { return eventList(sSearchCache.get(searchTerms)); } public static void fetchAcademicCalendar(final int year, final int month, Context context, final Handler uiHandler) { if(getAcademicCalendar(year, month) != null) { MobileWebApi.sendSuccessMessage(uiHandler); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "academic"); eventParameters.put("year", Integer.toString(year)); eventParameters.put("month", Integer.toString(month)); webApi.requestJSONArray(eventParameters, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray jArray) { List<EventDetailsItem> events = parseDetailArray(jArray); putInAcademicCalendarCache(year, month, events); MobileWebApi.sendSuccessMessage(uiHandler); } }); } /* * Keys are year such as (2010) then month (0-11) * values are List of eventIds * */ private static HashMap<Integer, HashMap<Integer, List<String>>> sAcademicCalendarCache = new HashMap<Integer, HashMap<Integer, List<String>>>(); private static void putInAcademicCalendarCache(int year, int month, List<EventDetailsItem> events) { List<String> eventIds = eventIds(events); if(!sAcademicCalendarCache.containsKey(year)) { sAcademicCalendarCache.put(year, new HashMap<Integer, List<String>>()); } sAcademicCalendarCache.get(year).put(month, eventIds); } public static List<EventDetailsItem> getAcademicCalendar(int year, int month) { HashMap<Integer, List<String>> yearCache = sAcademicCalendarCache.get(year); if(yearCache == null) { return null; } if(!yearCache.containsKey(month)) { return null; } return eventList(yearCache.get(month)); } public static EventDetailsItem getBriefEventDetails(int eventId) { return sBriefEventsCache.get(eventId); } private static List<String> sHolidayIds; public static void fetchHolidays(Context context, final Handler uiHandler) { if(sHolidayIds != null) { MobileWebApi.sendSuccessMessage(uiHandler); return; } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "holidays"); webApi.requestJSONArray(eventParameters, new MobileWebApi.JSONArrayResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONArray jArray) { List<EventDetailsItem> events = parseDetailArray(jArray); sHolidayIds = eventIds(events); MobileWebApi.sendSuccessMessage(uiHandler); } }); } public static List<EventDetailsItem> getHolidays() { return eventList(sHolidayIds); } public static void fetchEventDetails(final String eventId, final Context context, final Handler uiHandler) { if(sFullEventsCache.containsKey(eventId)) { MobileWebApi.sendSuccessMessage(uiHandler); } MobileWebApi webApi = new MobileWebApi(false, true, "Calendar", context, uiHandler); HashMap<String, String> eventParameters = new HashMap<String, String>(); eventParameters.put("module", "calendar"); eventParameters.put("command", "detail"); eventParameters.put("id", eventId); webApi.requestJSONObject(eventParameters, new MobileWebApi.JSONObjectResponseListener( new MobileWebApi.DefaultErrorListener(uiHandler), null) { @Override public void onResponse(JSONObject jObject) { EventDetailsItem event = parseDetailItem(jObject); sFullEventsCache.put(eventId, event); MobileWebApi.sendSuccessMessage(uiHandler); } }); } public static EventDetailsItem getFullEvent(String eventId) { return sFullEventsCache.get(eventId); } public static int getPosition(String eventId, List<EventDetailsItem> items) { for(int i = 0; i < items.size(); i++) { if(items.get(i).id.equals(eventId)) { return i; } } return -1; } /* * So strange the json parser seems to interpret the value null as * the string "null", so this litte wrapper works-around that issue */ private static String optString(JSONObject jObject, String fieldName) { String value = jObject.optString(fieldName, ""); if(value.equals("null")) { return ""; } return value; } private static Long optLong(JSONObject jObject, String fieldName) { if(jObject.has(fieldName)) { try { return jObject.getLong(fieldName); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } }