com.google.samples.apps.iosched.session.SessionDetailModel.java Source code

Java tutorial

Introduction

Here is the source code for com.google.samples.apps.iosched.session.SessionDetailModel.java

Source

/*
 * Copyright 2015 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.session;

import android.app.LoaderManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Pair;

import com.google.common.annotations.VisibleForTesting;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
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.feedback.SessionFeedbackActivity;
import com.google.samples.apps.iosched.model.TagMetadata;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.service.SessionAlarmService;
import com.google.samples.apps.iosched.service.SessionCalendarService;
import com.google.samples.apps.iosched.util.AccountUtils;
import com.google.samples.apps.iosched.util.AnalyticsHelper;
import com.google.samples.apps.iosched.util.ExtendedSessionHelper;
import com.google.samples.apps.iosched.util.SessionsHelper;
import com.google.samples.apps.iosched.util.TimeUtils;
import com.google.samples.apps.iosched.util.UIUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static com.google.samples.apps.iosched.util.LogUtils.LOGD;

import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

public class SessionDetailModel extends
        ModelWithLoaderManager<SessionDetailModel.SessionDetailQueryEnum, SessionDetailModel.SessionDetailUserActionEnum> {

    protected final static String TAG = makeLogTag(SessionDetailModel.class);

    /**
     * The cursor fields for the links. The corresponding resource ids for the links descriptions
     * are in  {@link #LINKS_DESCRIPTION_RESOURCE_IDS}.
     */
    private static final String[] LINKS_CURSOR_FIELDS = { ScheduleContract.Sessions.SESSION_YOUTUBE_URL,
            ScheduleContract.Sessions.SESSION_MODERATOR_URL, ScheduleContract.Sessions.SESSION_PDF_URL,
            ScheduleContract.Sessions.SESSION_NOTES_URL };

    /**
     * The resource ids for the links descriptions. The corresponding cursor fields for the links
     * are in {@link #LINKS_CURSOR_FIELDS}.
     */
    private static final int[] LINKS_DESCRIPTION_RESOURCE_IDS = { R.string.session_link_youtube,
            R.string.session_link_moderator, R.string.session_link_pdf, R.string.session_link_notes, };

    private final Context mContext;

    private final SessionsHelper mSessionsHelper;

    private String mSessionId;

    private Uri mSessionUri;

    private boolean mSessionLoaded = false;

    private String mTitle;

    private String mSubtitle;

    private int mSessionColor;

    private String mMainTag;

    private boolean mInSchedule;

    private boolean mInScheduleWhenSessionFirstLoaded;

    private boolean mIsKeynote;

    private long mSessionStart;

    private long mSessionEnd;

    private String mSessionAbstract;

    private String mHashTag;

    private String mUrl = "";

    private String mRoomId;

    private String mRoomName;

    private String mTagsString;

    private String mLiveStreamId;

    private String mYouTubeUrl;

    private String mPhotoUrl;

    private boolean mHasLiveStream = false;

    private boolean mLiveStreamVideoWatched = false;

    private boolean mHasFeedback = false;

    private String mRequirements;

    private String mSpeakersNames;

    private TagMetadata mTagMetadata;

    /**
     * Holds a list of links for the session. The first element of the {@code Pair} is the resource
     * id for the string describing the link, the second is the {@code Intent} to launch when
     * selecting the link.
     */
    private List<Pair<Integer, Intent>> mLinks = new ArrayList<Pair<Integer, Intent>>();

    private List<Speaker> mSpeakers = new ArrayList<Speaker>();

    private StringBuilder mBuffer = new StringBuilder();

    public SessionDetailModel(Uri sessionUri, Context context, SessionsHelper sessionsHelper,
            LoaderManager loaderManager) {
        super(SessionDetailQueryEnum.values(), SessionDetailUserActionEnum.values(), loaderManager);
        mContext = context;
        mSessionsHelper = sessionsHelper;
        mSessionUri = sessionUri;
    }

    public String getSessionId() {
        return mSessionId;
    }

    public String getSessionTitle() {
        return mTitle;
    }

    public String getSessionSubtitle() {
        return mSubtitle;
    }

    public String getSessionUrl() {
        return mUrl;
    }

    public String getRoomId() {
        return mRoomId;
    }

    public String getLiveStreamId() {
        return mLiveStreamId;
    }

    public String getYouTubeUrl() {
        return mYouTubeUrl;
    }

    public int getSessionColor() {
        return mSessionColor;
    }

    public boolean isSessionTrackColorAvailable() {
        return mTagMetadata != null && mMainTag != null;
    }

    public int getSessionTrackColor() {
        if (isSessionTrackColorAvailable()) {
            final TagMetadata.Tag tag = mTagMetadata.getTag(mMainTag);
            if (tag != null) {
                return tag.getColor();
            }
        }
        // else default to the session color which is defaulted to the theme primary color
        return mSessionColor;
    }

    public String getSessionAbstract() {
        return mSessionAbstract;
    }

    public boolean getLiveStreamVideoWatched() {
        return mLiveStreamVideoWatched;
    }

    public boolean isSessionOngoing() {
        long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
        return currentTimeMillis > mSessionStart && currentTimeMillis <= mSessionEnd;
    }

    /**
     * Live stream should be shown if url is available and the session will start in no more than 10
     * minutes, or is ongoing or has ended.
     */
    public boolean showLiveStream() {
        if (!hasLiveStream()) {
            return false;
        }
        long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
        return currentTimeMillis > mSessionStart - SessionDetailConstants.LIVESTREAM_BEFORE_SESSION_START_MS;
    }

    public boolean hasSessionStarted() {
        long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
        return currentTimeMillis > mSessionStart;
    }

    public boolean hasSessionEnded() {
        long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
        return currentTimeMillis > mSessionEnd;
    }

    /**
     * Returns the number of minutes, rounded down, since session has started, or 0 if not started
     * yet.
     */
    public long minutesSinceSessionStarted() {
        if (!hasSessionStarted()) {
            return 0l;
        } else {
            long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
            // Rounded down number of minutes.
            return (currentTimeMillis - mSessionStart) / 60000;
        }
    }

    /**
     * Returns the number of minutes, rounded up, until session stars, or 0 if already started.
     */
    public long minutesUntilSessionStarts() {
        if (hasSessionStarted()) {
            return 0l;
        } else {
            long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
            int minutes = (int) ((mSessionStart - currentTimeMillis) / 60000);
            // Rounded up number of minutes.
            return minutes * 60000 < (mSessionStart - currentTimeMillis) ? minutes + 1 : minutes;
        }
    }

    public long minutesUntilSessionEnds() {
        if (hasSessionEnded()) {
            // If session has ended, return 0 minutes until end of session.
            return 0l;
        } else {
            long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
            int minutes = (int) ((mSessionEnd - currentTimeMillis) / 60000);
            // Rounded up number of minutes.
            return minutes * 60000 < (mSessionEnd - currentTimeMillis) ? minutes + 1 : minutes;
        }
    }

    public boolean shouldShowExtendedSessionLink() {
        // If display of link is conditional, place conditions here.
        // For instance if it should only be shown during a session, use isSessionOngoing().
        return ExtendedSessionHelper.shouldShowExtendedSessionLink();
    }

    public boolean isSessionReadyForFeedback() {
        long currentTimeMillis = TimeUtils.getCurrentTime(mContext);
        return currentTimeMillis > mSessionEnd - SessionDetailConstants.FEEDBACK_MILLIS_BEFORE_SESSION_END_MS;
    }

    public boolean hasLiveStream() {
        return mHasLiveStream || (!TextUtils.isEmpty(mYouTubeUrl) && !mYouTubeUrl.equals("null"));
    }

    /**
     * Show header image if it has a photo url and it is keynote session, or has a youTube
     * url or has a live stream for a session that is about to start, is ongoing, or has ended.
     */
    public boolean shouldShowHeaderImage() {
        boolean hasYouTubeUrl = !TextUtils.isEmpty(mYouTubeUrl) && !mYouTubeUrl.equals("null");
        return hasPhotoUrl() && (isKeynote() || hasYouTubeUrl || showLiveStream());
    }

    public boolean isInSchedule() {
        return mInSchedule;
    }

    public boolean isInScheduleWhenSessionFirstLoaded() {
        return mInScheduleWhenSessionFirstLoaded;
    }

    public boolean isKeynote() {
        return mIsKeynote;
    }

    public boolean hasFeedback() {
        return mHasFeedback;
    }

    public boolean hasPhotoUrl() {
        return !TextUtils.isEmpty(mPhotoUrl);
    }

    public String getPhotoUrl() {
        return mPhotoUrl;
    }

    public String getRequirements() {
        return mRequirements;
    }

    public String getHashTag() {
        return mHashTag;
    }

    public TagMetadata getTagMetadata() {
        return mTagMetadata;
    }

    public String getTagsString() {
        return mTagsString;
    }

    public List<Pair<Integer, Intent>> getLinks() {
        return mLinks;
    }

    public List<Speaker> getSpeakers() {
        return mSpeakers;
    }

    public boolean hasSummaryContent() {
        return !TextUtils.isEmpty(mSessionAbstract) || !TextUtils.isEmpty(mRequirements);
    }

    @Override
    public boolean readDataFromCursor(Cursor cursor, SessionDetailQueryEnum query) {
        boolean success = false;

        if (cursor != null && cursor.moveToFirst()) {

            if (SessionDetailQueryEnum.SESSIONS == query) {
                readDataFromSessionCursor(cursor);
                mSessionLoaded = true;
                success = true;
            } else if (SessionDetailQueryEnum.TAG_METADATA == query) {
                readDataFromTagMetadataCursor(cursor);
                success = true;
            } else if (SessionDetailQueryEnum.FEEDBACK == query) {
                readDataFromFeedbackCursor(cursor);
                success = true;
            } else if (SessionDetailQueryEnum.SPEAKERS == query) {
                readDataFromSpeakersCursor(cursor);
                success = true;
            } else if (SessionDetailQueryEnum.MY_VIEWED_VIDEOS == query) {
                readDataFromMyViewedVideosCursor(cursor);
                success = true;
            }
        }

        return success;
    }

    private void readDataFromMyViewedVideosCursor(Cursor cursor) {
        String videoID = cursor.getString(cursor.getColumnIndex(ScheduleContract.MyViewedVideos.VIDEO_ID));
        if (videoID != null && videoID.equals(mLiveStreamId)) {
            mLiveStreamVideoWatched = true;
        }
    }

    private void readDataFromSessionCursor(Cursor cursor) {
        mTitle = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_TITLE));

        mInSchedule = cursor.getInt(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE)) != 0;
        if (!mSessionLoaded) {
            mInScheduleWhenSessionFirstLoaded = mInSchedule;
        }
        mTagsString = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_TAGS));
        mIsKeynote = mTagsString != null && mTagsString.contains(Config.Tags.SPECIAL_KEYNOTE);

        mSessionColor = cursor.getInt(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_COLOR));
        if (mSessionColor == 0) {
            mSessionColor = ContextCompat.getColor(mContext, R.color.default_session_color);
        } else {
            mSessionColor = UIUtils.setColorOpaque(mSessionColor);
        }

        mMainTag = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_MAIN_TAG));

        mLiveStreamId = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_LIVESTREAM_ID));

        mHasLiveStream = !TextUtils.isEmpty(mLiveStreamId);

        mYouTubeUrl = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_YOUTUBE_URL));

        mSessionStart = cursor.getLong(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_START));
        mSessionEnd = cursor.getLong(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_END));

        mRoomName = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.ROOM_NAME));
        mRoomId = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.ROOM_ID));

        mHashTag = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_HASHTAG));

        mPhotoUrl = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_PHOTO_URL));
        mUrl = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_URL));

        mSessionAbstract = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_ABSTRACT));

        mSpeakersNames = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_SPEAKER_NAMES));

        mRequirements = cursor.getString(cursor.getColumnIndex(ScheduleContract.Sessions.SESSION_REQUIREMENTS));

        formatSubtitle();

        buildLinks(cursor);
    }

    @VisibleForTesting
    public void formatSubtitle() {
        mSubtitle = UIUtils.formatSessionSubtitle(mSessionStart, mSessionEnd, mRoomName, mBuffer, mContext);
        if (mHasLiveStream) {
            mSubtitle += " " + UIUtils.getLiveBadgeText(mContext, mSessionStart, mSessionEnd);
        }
    }

    @VisibleForTesting
    public String getExtendedSessionUrl() {
        return ExtendedSessionHelper.getExtendedSessionUrl(this);
    }

    private void buildLinks(Cursor cursor) {
        mLinks.clear();

        if (hasLiveStream() && isSessionOngoing()) {
            mLinks.add(new Pair<Integer, Intent>(R.string.session_link_livestream, getWatchLiveIntent()));
        }

        if (!hasFeedback() && isSessionReadyForFeedback()) {
            mLinks.add(new Pair<Integer, Intent>(R.string.session_feedback_submitlink, getFeedbackIntent()));
        }

        for (int i = 0; i < LINKS_CURSOR_FIELDS.length; i++) {
            final String linkUrl = cursor.getString(cursor.getColumnIndex(LINKS_CURSOR_FIELDS[i]));
            if (TextUtils.isEmpty(linkUrl)) {
                continue;
            }

            mLinks.add(new Pair<Integer, Intent>(LINKS_DESCRIPTION_RESOURCE_IDS[i],
                    new Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl)).addFlags(getFlagForUrlLink())));
        }
    }

    private int getFlagForUrlLink() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            return Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
        } else {
            return Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
        }
    }

    public Intent getWatchLiveIntent() {
        final String youtubeLink = String.format(Locale.US, Config.VIDEO_LIBRARY_URL_FMT,
                TextUtils.isEmpty(mLiveStreamId) ? "" : mLiveStreamId);
        return new Intent(Intent.ACTION_VIEW, Uri.parse(youtubeLink));
    }

    public Intent getFeedbackIntent() {
        return new Intent(Intent.ACTION_VIEW, mSessionUri, mContext, SessionFeedbackActivity.class);
    }

    private void readDataFromTagMetadataCursor(Cursor cursor) {
        mTagMetadata = new TagMetadata(cursor);
    }

    private void readDataFromFeedbackCursor(Cursor cursor) {
        mHasFeedback = cursor.getCount() > 0;
    }

    private void readDataFromSpeakersCursor(Cursor cursor) {
        mSpeakers.clear();

        // Not using while(cursor.moveToNext()) because it would lead to issues when writing tests.
        // Either we would mock cursor.moveToNext() to return true and the test would have infinite
        // loop, or we would mock cursor.moveToNext() to return false, and the test would be for an
        // empty cursor.
        int count = cursor.getCount();
        for (int i = 0; i < count; i++) {
            cursor.moveToPosition(i);
            final String speakerName = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_NAME));
            if (TextUtils.isEmpty(speakerName)) {
                continue;
            }

            final String speakerImageUrl = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_IMAGE_URL));
            final String speakerCompany = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_COMPANY));
            final String speakerUrl = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_URL));
            final String speakerPlusoneUrl = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_PLUSONE_URL));
            final String speakerTwitterUrl = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_TWITTER_URL));
            final String speakerAbstract = cursor
                    .getString(cursor.getColumnIndex(ScheduleContract.Speakers.SPEAKER_ABSTRACT));

            mSpeakers.add(new Speaker(speakerName, speakerImageUrl, speakerCompany, speakerUrl, speakerPlusoneUrl,
                    speakerTwitterUrl, speakerAbstract));
        }
    }

    @Override
    public Loader<Cursor> createCursorLoader(SessionDetailQueryEnum query, Bundle args) {
        CursorLoader loader = null;
        if (query == null) {
            return loader;
        }
        switch (query) {
        case SESSIONS:
            mSessionId = getSessionId(mSessionUri);
            loader = getCursorLoaderInstance(mContext, mSessionUri, SessionDetailQueryEnum.SESSIONS.getProjection(),
                    null, null, null);
            break;
        case SPEAKERS:
            Uri speakersUri = getSpeakersDirUri(mSessionId);
            loader = getCursorLoaderInstance(mContext, speakersUri, SessionDetailQueryEnum.SPEAKERS.getProjection(),
                    null, null, ScheduleContract.Speakers.DEFAULT_SORT);
            break;
        case FEEDBACK:
            Uri feedbackUri = getFeedbackUri(mSessionId);
            loader = getCursorLoaderInstance(mContext, feedbackUri, SessionDetailQueryEnum.FEEDBACK.getProjection(),
                    null, null, null);
            break;
        case TAG_METADATA:
            loader = getTagMetadataLoader();
            break;
        case MY_VIEWED_VIDEOS:
            Uri myPlayedVideoUri = ScheduleContract.MyViewedVideos
                    .buildMyViewedVideosUri(AccountUtils.getActiveAccountName(mContext));
            loader = getCursorLoaderInstance(mContext, myPlayedVideoUri,
                    SessionDetailQueryEnum.MY_VIEWED_VIDEOS.getProjection(), null, null, null);
            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);
    }

    @VisibleForTesting
    public CursorLoader getTagMetadataLoader() {
        return TagMetadata.createCursorLoader(mContext);
    }

    @VisibleForTesting
    public Uri getFeedbackUri(String sessionId) {
        return ScheduleContract.Feedback.buildFeedbackUri(sessionId);
    }

    @VisibleForTesting
    public Uri getSpeakersDirUri(String sessionId) {
        return ScheduleContract.Sessions.buildSpeakersDirUri(sessionId);
    }

    @VisibleForTesting
    public String getSessionId(Uri uri) {
        return ScheduleContract.Sessions.getSessionId(uri);
    }

    @Override
    public void processUserAction(SessionDetailUserActionEnum action,
            @android.support.annotation.Nullable Bundle args, UserActionCallback callback) {
        switch (action) {
        case STAR:
            mInSchedule = true;
            mSessionsHelper.setSessionStarred(mSessionUri, true, null);
            amendCalendarAndSetUpNotificationIfRequired();
            sendAnalyticsEventForStarUnstarSession(true);
            callback.onModelUpdated(this, action);
            break;
        case UNSTAR:
            mInSchedule = false;
            mSessionsHelper.setSessionStarred(mSessionUri, false, null);
            amendCalendarAndSetUpNotificationIfRequired();
            sendAnalyticsEventForStarUnstarSession(false);
            callback.onModelUpdated(this, action);
            break;
        case SHOW_MAP:
            // ANALYTICS EVENT: Click on Map action in Session Details page.
            // Contains: Session title/subtitle
            sendAnalyticsEvent("Session", "Map", mTitle);
            callback.onModelUpdated(this, action);
            break;
        case SHOW_SHARE:
            // ANALYTICS EVENT: Share a session.
            // Contains: Session title.
            sendAnalyticsEvent("Session", "Shared", mTitle);
            callback.onModelUpdated(this, action);
            break;
        case GIVE_FEEDBACK:
            // ANALYTICS EVENT: Click on the "send feedback" action in Session Details.
            // Contains: The session title.
            sendAnalyticsEvent("Session", "Feedback", getSessionTitle());
            callback.onModelUpdated(this, action);
            break;
        case EXTENDED:
            // ANALYTICS EVENT: Click on the extended session link in Session Details.
            sendAnalyticsEvent("Session", "Extended Session", getSessionTitle());
            callback.onModelUpdated(this, action);
            break;

        default:
            callback.onError(action);
        }
    }

    private void amendCalendarAndSetUpNotificationIfRequired() {
        if (!hasSessionStarted()) {
            Intent intent;
            if (mInSchedule) {
                intent = new Intent(SessionCalendarService.ACTION_ADD_SESSION_CALENDAR, mSessionUri);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_START, mSessionStart);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_END, mSessionEnd);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_ROOM, mRoomName);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_TITLE, mTitle);
            } else {
                intent = new Intent(SessionCalendarService.ACTION_REMOVE_SESSION_CALENDAR, mSessionUri);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_START, mSessionStart);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_END, mSessionEnd);
                intent.putExtra(SessionCalendarService.EXTRA_SESSION_TITLE, mTitle);
            }
            intent.setClass(mContext, SessionCalendarService.class);
            mContext.startService(intent);

            if (mInSchedule) {
                setUpNotification();
            }
        }
    }

    private void setUpNotification() {
        Intent scheduleIntent;

        // Schedule session notification
        if (!hasSessionStarted()) {
            LOGD(TAG, "Scheduling notification about session start.");
            scheduleIntent = new Intent(SessionAlarmService.ACTION_SCHEDULE_STARRED_BLOCK, null, mContext,
                    SessionAlarmService.class);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, mSessionStart);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, mSessionEnd);
            mContext.startService(scheduleIntent);
        } else {
            LOGD(TAG, "Not scheduling notification about session start, too late.");
        }

        // Schedule feedback notification
        if (!hasSessionEnded()) {
            LOGD(TAG, "Scheduling notification about session feedback.");
            scheduleIntent = new Intent(SessionAlarmService.ACTION_SCHEDULE_FEEDBACK_NOTIFICATION, null, mContext,
                    SessionAlarmService.class);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ID, mSessionId);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, mSessionStart);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, mSessionEnd);
            scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_TITLE, mTitle);
            mContext.startService(scheduleIntent);
        } else {
            LOGD(TAG, "Not scheduling feedback notification, too late.");
        }
    }

    @VisibleForTesting
    public void sendAnalyticsEvent(String category, String action, String label) {
        AnalyticsHelper.sendEvent(category, action, label);
    }

    private void sendAnalyticsEventForStarUnstarSession(boolean starred) {
        // ANALYTICS EVENT: Add or remove a session from My Schedule (starred vs unstarred)
        // Contains: Session title, whether it was added or removed (starred or unstarred)
        sendAnalyticsEvent("Session", starred ? "Starred" : "Unstarred", mTitle);
    }

    @Override
    public void cleanUp() {
        // Nothing to clean up
    }

    public static class Speaker {

        private String mName;

        private String mImageUrl;

        private String mCompany;

        private String mUrl;

        private String mPlusoneUrl;

        private String mTwitterUrl;

        private String mAbstract;

        public Speaker(String name, String imageUrl, String company, String url, String plusoneUrl,
                String twitterUrl, String anAbstract) {
            mName = name;
            mImageUrl = imageUrl;
            mCompany = company;
            mUrl = url;
            mPlusoneUrl = plusoneUrl;
            mTwitterUrl = twitterUrl;
            mAbstract = anAbstract;
        }

        public String getName() {
            return mName;
        }

        public String getImageUrl() {
            return mImageUrl;
        }

        public String getCompany() {
            return mCompany;
        }

        public String getUrl() {
            return mUrl;
        }

        public String getPlusoneUrl() {
            return mPlusoneUrl;
        }

        public String getTwitterUrl() {
            return mTwitterUrl;
        }

        public String getAbstract() {
            return mAbstract;
        }
    }

    public enum SessionDetailQueryEnum implements QueryEnum {
        SESSIONS(0, new String[] { ScheduleContract.Sessions.SESSION_START, ScheduleContract.Sessions.SESSION_END,
                ScheduleContract.Sessions.SESSION_LEVEL, ScheduleContract.Sessions.SESSION_TITLE,
                ScheduleContract.Sessions.SESSION_ABSTRACT, ScheduleContract.Sessions.SESSION_REQUIREMENTS,
                ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE, ScheduleContract.Sessions.SESSION_HASHTAG,
                ScheduleContract.Sessions.SESSION_URL, ScheduleContract.Sessions.SESSION_YOUTUBE_URL,
                ScheduleContract.Sessions.SESSION_PDF_URL, ScheduleContract.Sessions.SESSION_NOTES_URL,
                ScheduleContract.Sessions.SESSION_LIVESTREAM_ID, ScheduleContract.Sessions.SESSION_MODERATOR_URL,
                ScheduleContract.Sessions.ROOM_ID, ScheduleContract.Rooms.ROOM_NAME,
                ScheduleContract.Sessions.SESSION_COLOR, ScheduleContract.Sessions.SESSION_PHOTO_URL,
                ScheduleContract.Sessions.SESSION_RELATED_CONTENT, ScheduleContract.Sessions.SESSION_TAGS,
                ScheduleContract.Sessions.SESSION_SPEAKER_NAMES,
                ScheduleContract.Sessions.SESSION_MAIN_TAG }), SPEAKERS(
                        1,
                        new String[] { ScheduleContract.Speakers.SPEAKER_NAME,
                                ScheduleContract.Speakers.SPEAKER_IMAGE_URL,
                                ScheduleContract.Speakers.SPEAKER_COMPANY,
                                ScheduleContract.Speakers.SPEAKER_ABSTRACT, ScheduleContract.Speakers.SPEAKER_URL,
                                ScheduleContract.Speakers.SPEAKER_PLUSONE_URL,
                                ScheduleContract.Speakers.SPEAKER_TWITTER_URL }), FEEDBACK(2,
                                        new String[] { ScheduleContract.Feedback.SESSION_ID }), TAG_METADATA(3,
                                                null), MY_VIEWED_VIDEOS(4,
                                                        new String[] { ScheduleContract.MyViewedVideos.VIDEO_ID });

        private int id;

        private String[] projection;

        SessionDetailQueryEnum(int id, String[] projection) {
            this.id = id;
            this.projection = projection;
        }

        @Override
        public int getId() {
            return id;
        }

        @Override
        public String[] getProjection() {
            return projection;
        }

    }

    public enum SessionDetailUserActionEnum implements UserActionEnum {
        STAR(1), UNSTAR(2), SHOW_MAP(3), SHOW_SHARE(4), GIVE_FEEDBACK(5), EXTENDED(6);

        private int id;

        SessionDetailUserActionEnum(int id) {
            this.id = id;
        }

        @Override
        public int getId() {
            return id;
        }

    }
}