/*
* 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.explore;
import android.app.LoaderManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.archframework.Model;
import com.google.samples.apps.iosched.archframework.ModelWithLoaderManager;
import com.google.samples.apps.iosched.archframework.QueryEnum;
import com.google.samples.apps.iosched.archframework.UserActionEnum;
import com.google.samples.apps.iosched.explore.data.EventCard;
import com.google.samples.apps.iosched.explore.data.EventData;
import com.google.samples.apps.iosched.explore.data.ItemGroup;
import com.google.samples.apps.iosched.explore.data.LiveStreamData;
import com.google.samples.apps.iosched.explore.data.MessageData;
import com.google.samples.apps.iosched.explore.data.SessionData;
import com.google.samples.apps.iosched.model.TagMetadata;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.settings.ConfMessageCardUtils;
import com.google.samples.apps.iosched.settings.SettingsUtils;
import com.google.samples.apps.iosched.util.TagUtils;
import com.google.samples.apps.iosched.util.TimeUtils;
import com.google.samples.apps.iosched.util.WiFiUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.LOGI;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* This is an implementation of a {@link Model} that queries the sessions at Google I/O and extracts
* the data needed to present the Explore I/O user interface.
*/
public class ExploreIOModel extends ModelWithLoaderManager<ExploreIOModel.ExploreIOQueryEnum,
ExploreIOModel.ExploreIOUserActionEnum> {
private static final String TAG = makeLogTag(ExploreIOModel.class);
private final Context mContext;
@NonNull
private EventData mEventData = new EventData();
private SessionData mKeynoteData;
private LiveStreamData mLiveStreamData;
private List<ItemGroup> mOrderedTracks;
private Uri mSessionsUri;
private TagMetadata mTagMetadata;
/**
* Theme groups loaded from the database pre-randomly filtered and stored by topic name. Not
* shown in current design.
*/
private Map<String, ItemGroup> mThemes = new HashMap<>();
/**
* Topic groups loaded from the database pre-randomly filtered and stored by topic name.
*/
private Map<String, ItemGroup> mTracks = new HashMap<>();
public ExploreIOModel(Context context, Uri sessionsUri, LoaderManager loaderManager) {
super(ExploreIOQueryEnum.values(), ExploreIOUserActionEnum.values(), loaderManager);
mContext = context;
mSessionsUri = sessionsUri;
}
@Override
public void cleanUp() {
mThemes.clear();
mThemes = null;
mTracks.clear();
mTracks = null;
mOrderedTracks.clear();
mOrderedTracks = null;
mKeynoteData = null;
mLiveStreamData = null;
}
@Override
public Loader<Cursor> createCursorLoader(final ExploreIOQueryEnum query, final Bundle args) {
CursorLoader loader = null;
switch (query) {
case SESSIONS:
// Create and return the Loader.
loader = getCursorLoaderInstance(mContext, mSessionsUri,
ExploreIOQueryEnum.SESSIONS.getProjection(), null, null,
ScheduleContract.Sessions.SORT_BY_TYPE_THEN_TIME);
break;
case TAGS:
LOGI(TAG, "Starting sessions tag query");
loader = TagMetadata.createCursorLoader(mContext);
break;
case CARDS:
String currentTime = TimeUtils.getCurrentTime(mContext) + "";
LOGI(TAG, "Starting cards query: " + currentTime);
loader = getCursorLoaderInstance(mContext, ScheduleContract.Cards.CONTENT_URI,
ExploreIOQueryEnum.CARDS.getProjection(),
" ? > " + ScheduleContract.Cards.DISPLAY_START_DATE + " AND ? < " +
ScheduleContract.Cards.DISPLAY_END_DATE + " AND " +
ScheduleContract.Cards.ACTION_TYPE + " IN ('" +
EventCard.ACTION_TYPE_LINK + "', '" +
EventCard.ACTION_TYPE_MAP + "', '" +
EventCard.ACTION_TYPE_SESSION + "')",
new String[]{currentTime, currentTime},
ScheduleContract.Cards.CARD_ID);
break;
}
return loader;
}
@VisibleForTesting
public CursorLoader getCursorLoaderInstance(Context context, Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
return new CursorLoader(context, uri, projection, selection, selectionArgs, sortOrder);
}
@NonNull
public EventData getEventData() {
return mEventData;
}
public SessionData getKeynoteData() { return mKeynoteData; }
public LiveStreamData getLiveStreamData() { return mLiveStreamData; }
/**
* Get the list of {@link MessageData} to be displayed to the user, based upon time, location
* etc.
*
* @return messages to be displayed.
*/
public List<MessageData> getMessages() {
final List<MessageData> messagesToDisplay = new ArrayList<>();
if (shouldShowCard(ConfMessageCardUtils.ConfMessageCard.SESSION_NOTIFICATIONS)) {
messagesToDisplay.add(MessageCardHelper.getNotificationsOptInMessageData());
}
if (SettingsUtils.isAttendeeAtVenue(mContext)) {
// Users are required to opt in or out of whether they want conference message cards
if (!ConfMessageCardUtils.hasAnsweredConfMessageCardsPrompt(mContext)) {
// User has not answered whether they want to opt in.
// Build a opt-in/out card.
messagesToDisplay.add(MessageCardHelper.getConferenceOptInMessageData());
return messagesToDisplay;
}
if (ConfMessageCardUtils.isConfMessageCardsEnabled(mContext)) {
LOGD(TAG, "Conf cards Enabled");
// User has answered they want to opt in AND the message cards are enabled.
ConfMessageCardUtils.enableActiveCards(mContext);
// Note that for these special cards, we'll never show more than one at a time
// to prevent overloading the user with messagesToDisplay.
// We want each new message to be notable.
if (shouldShowCard(ConfMessageCardUtils.ConfMessageCard.WIFI_PRELOAD)) {
// Check whether a wifi setup card should be offered.
if (WiFiUtils.shouldOfferToSetupWifi(mContext, true)) {
// Build card asking users whether they want to enable wifi.
messagesToDisplay.add(MessageCardHelper.getWifiSetupMessageData());
return messagesToDisplay;
}
}
if (messagesToDisplay.size() < 1) {
LOGD(TAG, "Simple cards");
List<ConfMessageCardUtils.ConfMessageCard> simpleCards =
ConfMessageCardUtils.ConfMessageCard.getActiveSimpleCards(mContext);
// Only show a single card at a time.
if (simpleCards.size() > 0) {
messagesToDisplay.add(MessageCardHelper.getSimpleMessageCardData(
simpleCards.get(0)));
}
}
}
}
return messagesToDisplay;
}
/**
* @return the tracks ordered alphabetically. The ordering can only happen if the query {@link
* com.google.samples.apps.iosched.explore.ExploreIOModel.ExploreIOQueryEnum#TAGS} has returned,
* which can be checked by calling {@link #getTagMetadata()}.
*/
public Collection<ItemGroup> getOrderedTracks() {
if (mOrderedTracks != null) {
return mOrderedTracks;
}
mOrderedTracks = new ArrayList<ItemGroup>(getTracks());
for (ItemGroup item : mOrderedTracks) {
if (item.getTitle() == null) {
item.formatTitle(mTagMetadata);
}
}
// Order the tracks by title.
Collections.sort(mOrderedTracks, new Comparator<ItemGroup>() {
@Override
public int compare(final ItemGroup lhs, final ItemGroup rhs) {
if (lhs.getTitle() == null) {
return 1;
} else if (rhs.getTitle() == null) {
return -1;
}
return lhs.getTitle().compareTo(rhs.getTitle());
}
});
return mOrderedTracks;
}
public TagMetadata getTagMetadata() { return mTagMetadata; }
@Override
public void processUserAction(final ExploreIOUserActionEnum action,
@Nullable final Bundle args, final UserActionCallback callback) {
/**
* The only user action in this model fires off a query (using {@link #KEY_RUN_QUERY_ID},
* so this method isn't used.
*/
}
@Override
public boolean readDataFromCursor(final Cursor cursor, final ExploreIOQueryEnum query) {
switch (query) {
case SESSIONS:
readDataFromSessionsCursor(cursor);
return true;
case TAGS:
readDataFromTagsCursor(cursor);
return true;
case CARDS:
readDataFromCardsCursor(cursor);
return true;
}
return false;
}
private void addPhotoUrlToTopicsAndThemes() {
if (mTracks != null) {
for (ItemGroup topic : mTracks.values()) {
if (mTagMetadata != null && mTagMetadata.getTag(topic.getTitleId()) != null) {
topic.setPhotoUrl(mTagMetadata.getTag(topic.getTitleId()).getPhotoUrl());
}
}
}
if (mThemes != null) {
for (ItemGroup theme : mThemes.values()) {
if (mTagMetadata != null && mTagMetadata.getTag(theme.getTitleId()) != null) {
theme.setPhotoUrl(mTagMetadata.getTag(theme.getTitleId()).getPhotoUrl());
}
}
}
}
private Collection<ItemGroup> getThemes() {
return mThemes.values();
}
private Collection<ItemGroup> getTracks() {
return mTracks.values();
}
/**
* A session missing title, description, id, or image isn't eligible for the Explore screen.
*/
private boolean isSessionDataInvalid(SessionData session) {
return TextUtils.isEmpty(session.getSessionName()) ||
TextUtils.isEmpty(session.getDetails()) ||
TextUtils.isEmpty(session.getSessionId()) ||
TextUtils.isEmpty(session.getImageUrl());
}
private void populateSessionFromCursorRow(SessionData session, Cursor cursor) {
session.updateData(mContext,
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_TITLE)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_ABSTRACT)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_ID)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_PHOTO_URL)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_MAIN_TAG)),
cursor.getLong(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_START)),
cursor.getLong(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_END)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_LIVESTREAM_ID)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_YOUTUBE_URL)),
cursor.getString(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_TAGS)),
cursor.getLong(cursor.getColumnIndex(
ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE)) == 1L);
}
private void readDataFromCardsCursor(Cursor cursor) {
LOGD(TAG, "Cards query loaded");
mEventData = new EventData();
if (cursor != null && cursor.moveToFirst()) {
LOGD(TAG, "Read card data");
do {
EventCard card = EventCard.fromCursorRow(cursor);
if (card != null) {
mEventData.addEventCard(card);
}
} while (cursor.moveToNext());
LOGI(TAG, "Cards loaded: " + mEventData.getCards().size());
} else {
LOGE(TAG, "No Cards data");
}
}
/**
* As we go through the session query results we will be collecting X numbers of session data
* per Topic and Y numbers of sessions per Theme. When new topics or themes are seen a group
* will be created.
* <p/>
* As we iterate through the list of sessions we are also watching out for the keynote and any
* live sessions streaming right now.
*/
private void readDataFromSessionsCursor(Cursor cursor) {
LOGD(TAG, "Reading session data from cursor.");
boolean atVenue = SettingsUtils.isAttendeeAtVenue(mContext);
LiveStreamData liveStreamData = new LiveStreamData();
Map<String, ItemGroup> trackGroups = new HashMap<>();
Map<String, ItemGroup> themeGroups = new HashMap<>();
if (cursor != null && cursor.moveToFirst()) {
do {
SessionData session = new SessionData();
populateSessionFromCursorRow(session, cursor);
if (isSessionDataInvalid(session)) {
continue;
}
if (!atVenue &&
(!session.isLiveStreamAvailable()) && !session.isVideoAvailable()) {
// Skip the opportunity to present the session for those not on site
// since it won't be viewable as there is neither a live stream nor video
// available.
continue;
}
String tags = session.getTags();
if (Config.Tags.SPECIAL_KEYNOTE.equals(session.getMainTag())) {
SessionData keynoteData = new SessionData();
populateSessionFromCursorRow(keynoteData, cursor);
rewriteKeynoteDetails(keynoteData);
mKeynoteData = keynoteData;
} else if (session.isLiveStreamNow(mContext)) {
liveStreamData.addSessionData(session);
}
if (!TextUtils.isEmpty(tags)) {
StringTokenizer tagsTokenizer = new StringTokenizer(tags, ",");
while (tagsTokenizer.hasMoreTokens()) {
String rawTag = tagsTokenizer.nextToken();
if (TagUtils.isTrackTag(rawTag)) {
ItemGroup trackGroup = trackGroups.get(rawTag);
if (trackGroup == null) {
trackGroup = new ItemGroup();
trackGroup.setTitleId(rawTag);
trackGroup.setId(rawTag);
if (mTagMetadata != null && mTagMetadata.getTag(rawTag) != null) {
trackGroup
.setPhotoUrl(mTagMetadata.getTag(rawTag).getPhotoUrl());
}
trackGroups.put(rawTag, trackGroup);
}
trackGroup.addSessionData(session);
} else if (TagUtils.isThemeTag(rawTag)) {
ItemGroup themeGroup = themeGroups.get(rawTag);
if (themeGroup == null) {
themeGroup = new ItemGroup();
themeGroup.setTitleId(rawTag);
themeGroup.setId(rawTag);
if (mTagMetadata != null && mTagMetadata.getTag(rawTag) != null) {
themeGroup
.setPhotoUrl(mTagMetadata.getTag(rawTag).getPhotoUrl());
}
themeGroups.put(rawTag, themeGroup);
}
themeGroup.addSessionData(session);
}
}
}
} while (cursor.moveToNext());
}
if (liveStreamData.getSessions().size() > 0) {
mLiveStreamData = liveStreamData;
}
mThemes = themeGroups;
mTracks = trackGroups;
mOrderedTracks = null;
}
private void readDataFromTagsCursor(Cursor cursor) {
LOGD(TAG, "TAGS query loaded");
if (cursor != null && cursor.moveToFirst()) {
mTagMetadata = new TagMetadata(cursor);
}
addPhotoUrlToTopicsAndThemes();
}
private void rewriteKeynoteDetails(SessionData keynoteData) {
long startTime, endTime, currentTime;
currentTime = TimeUtils.getCurrentTime(mContext);
if (keynoteData.getStartDate() != null) {
startTime = keynoteData.getStartDate().getTimeInMillis();
} else {
LOGD(TAG, "Keynote start time wasn't set");
startTime = 0;
}
if (keynoteData.getEndDate() != null) {
endTime = keynoteData.getEndDate().getTimeInMillis();
} else {
LOGD(TAG, "Keynote end time wasn't set");
endTime = Long.MAX_VALUE;
}
StringBuilder stringBuilder = new StringBuilder();
if (currentTime >= startTime && currentTime < endTime) {
stringBuilder.append(mContext.getString(R.string
.live_now));
} else {
stringBuilder.append(
TimeUtils.formatShortDateTime(mContext, keynoteData.getStartDate().getTime()));
}
keynoteData.setDetails(stringBuilder.toString());
}
/**
* Check if this card should be shown and hasn't previously been dismissed.
*
* @return {@code true} if the given message card should be displayed.
*/
private boolean shouldShowCard(ConfMessageCardUtils.ConfMessageCard card) {
return ConfMessageCardUtils.shouldShowConfMessageCard(mContext, card) &&
!ConfMessageCardUtils.hasDismissedConfMessageCard(mContext, card);
}
/**
* Enumeration of the possible queries that can be done by this Model to retrieve data.
*/
public static enum ExploreIOQueryEnum implements QueryEnum {
/**
* Query that retrieves a list of sessions.
* <p/>
* Once the data has been loaded it can be retrieved using {@code getThemes()} and {@code
* getTracks()}.
*/
SESSIONS(0x1, new String[]{
ScheduleContract.Sessions.SESSION_ID,
ScheduleContract.Sessions.SESSION_TITLE,
ScheduleContract.Sessions.SESSION_ABSTRACT,
ScheduleContract.Sessions.SESSION_TAGS,
ScheduleContract.Sessions.SESSION_MAIN_TAG,
ScheduleContract.Sessions.SESSION_PHOTO_URL,
ScheduleContract.Sessions.SESSION_START,
ScheduleContract.Sessions.SESSION_END,
ScheduleContract.Sessions.SESSION_LIVESTREAM_ID,
ScheduleContract.Sessions.SESSION_YOUTUBE_URL,
ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE,
ScheduleContract.Sessions.SESSION_START,
ScheduleContract.Sessions.SESSION_END,
}),
TAGS(0x2, new String[]{
ScheduleContract.Tags.TAG_ID,
ScheduleContract.Tags.TAG_NAME,
}),
CARDS(0x3, new String[]{
ScheduleContract.Cards.CARD_ID,
ScheduleContract.Cards.TITLE,
ScheduleContract.Cards.TEXT_COLOR,
ScheduleContract.Cards.MESSAGE,
ScheduleContract.Cards.DISPLAY_END_DATE,
ScheduleContract.Cards.ACTION_COLOR,
ScheduleContract.Cards.ACTION_URL,
ScheduleContract.Cards.ACTION_TEXT,
ScheduleContract.Cards.BACKGROUND_COLOR,
ScheduleContract.Cards.DISPLAY_START_DATE,
ScheduleContract.Cards.ACTION_TYPE,
ScheduleContract.Cards.ACTION_EXTRA
});
private int id;
private String[] projection;
ExploreIOQueryEnum(int id, String[] projection) {
this.id = id;
this.projection = projection;
}
@Override
public int getId() {
return id;
}
@Override
public String[] getProjection() {
return projection;
}
}
/**
* Enumeration of the possible events that a user can trigger that would affect the state of the
* date of this Model.
*/
public static enum ExploreIOUserActionEnum implements UserActionEnum {
/**
* Event that is triggered when a user re-enters the video library this triggers a reload so
* that we can display another set of randomly selected videos.
*/
RELOAD(1);
private int id;
ExploreIOUserActionEnum(int id) {
this.id = id;
}
@Override
public int getId() {
return id;
}
}
}