com.google.samples.apps.iosched.service.SessionAlarmService.java Source code

Java tutorial

Introduction

Here is the source code for com.google.samples.apps.iosched.service.SessionAlarmService.java

Source

/*
 * 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.service;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.explore.ExploreIOActivity;
import com.google.samples.apps.iosched.feedback.FeedbackHelper;
import com.google.samples.apps.iosched.feedback.SessionFeedbackActivity;
import com.google.samples.apps.iosched.map.MapActivity;
import com.google.samples.apps.iosched.myschedule.MyScheduleActivity;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.provider.ScheduleContractHelper;
import com.google.samples.apps.iosched.settings.SettingsUtils;
import com.google.samples.apps.iosched.util.UIUtils;

import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

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.LOGW;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

/**
 * Background service to handle scheduling of starred session notification via
 * {@link android.app.AlarmManager}. The service also handles invoking the system notifications to
 * provide feedback for the starred sessions.
 */
public class SessionAlarmService extends IntentService
        implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {

    private static final String TAG = makeLogTag(SessionAlarmService.class);

    public static final String ACTION_NOTIFY_SESSION = "com.google.samples.apps.iosched.action.NOTIFY_SESSION";
    public static final String ACTION_NOTIFY_SESSION_FEEDBACK = "com.google.samples.apps.iosched.action.NOTIFY_SESSION_FEEDBACK";
    public static final String ACTION_SCHEDULE_FEEDBACK_NOTIFICATION = "com.google.samples.apps.iosched.action.SCHEDULE_FEEDBACK_NOTIFICATION";
    public static final String ACTION_SCHEDULE_STARRED_BLOCK = "com.google.samples.apps.iosched.action.SCHEDULE_STARRED_BLOCK";
    public static final String ACTION_SCHEDULE_ALL_STARRED_BLOCKS = "com.google.samples.apps.iosched.action.SCHEDULE_ALL_STARRED_BLOCKS";
    public static final String EXTRA_SESSION_START = "com.google.samples.apps.iosched.extra.SESSION_START";
    public static final String EXTRA_SESSION_END = "com.google.samples.apps.iosched.extra.SESSION_END";
    public static final String EXTRA_SESSION_ALARM_OFFSET = "com.google.samples.apps.iosched.extra.SESSION_ALARM_OFFSET";
    public static final String EXTRA_SESSION_ID = "com.google.samples.apps.iosched.extra.SESSION_ID";
    public static final String EXTRA_SESSION_TITLE = "com.google.samples.apps.iosched.extra.SESSION_TITLE";

    public static final int NOTIFICATION_ID = 100;
    public static final int FEEDBACK_NOTIFICATION_ID = 101;

    // pulsate every 1 second, indicating a relatively high degree of urgency
    private static final int NOTIFICATION_LED_ON_MS = 100;
    private static final int NOTIFICATION_LED_OFF_MS = 1000;
    private static final int NOTIFICATION_ARGB_COLOR = 0xff0088ff; // cyan

    private static final long MILLI_TEN_MINUTES = 600000;
    private static final long MILLI_FIVE_MINUTES = 300000;
    private static final long MILLI_ONE_MINUTE = 60000;

    private static final long UNDEFINED_ALARM_OFFSET = -1;
    private static final long UNDEFINED_VALUE = -1;
    public static final String ACTION_NOTIFICATION_DISMISSAL = "com.google.sample.apps.iosched.ACTION_NOTIFICATION_DISMISSAL";
    private GoogleApiClient mGoogleApiClient;
    public static final String KEY_SESSION_ID = "session-id";
    private static final String KEY_SESSION_NAME = "session-name";
    private static final String KEY_SPEAKER_NAME = "speaker-name";
    private static final String KEY_SESSION_ROOM = "session-room";
    public static final String PATH_FEEDBACK = "/iowear/feedback";

    // special session ID that identifies a debug notification
    public static final String DEBUG_SESSION_ID = "debug-session-id";

    public SessionAlarmService() {
        super(TAG);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this).build();
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        mGoogleApiClient.blockingConnect(2000, TimeUnit.MILLISECONDS);
        final String action = intent.getAction();

        LOGD(TAG, "SessionAlarmService handling " + action);

        if (ACTION_SCHEDULE_ALL_STARRED_BLOCKS.equals(action)) {
            LOGD(TAG, "Scheduling all starred blocks.");
            scheduleAllStarredBlocks();
            scheduleAllStarredSessionFeedbacks();
            return;
        } else if (ACTION_NOTIFY_SESSION_FEEDBACK.equals(action)) {
            LOGD(TAG, "Showing session feedback notification.");
            notifySessionFeedback(DEBUG_SESSION_ID.equals(intent.getStringExtra(EXTRA_SESSION_ID)));
            return;
        }

        final long sessionEnd = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_END, UNDEFINED_VALUE);
        if (sessionEnd == UNDEFINED_VALUE) {
            LOGD(TAG, "IGNORING ACTION -- missing sessionEnd parameter");
            return;
        }

        final long sessionAlarmOffset = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET,
                UNDEFINED_ALARM_OFFSET);
        LOGD(TAG, "Session alarm offset is: " + sessionAlarmOffset);

        // Feedback notifications have a slightly different set of extras.
        if (ACTION_SCHEDULE_FEEDBACK_NOTIFICATION.equals(action)) {
            final String sessionId = intent.getStringExtra(SessionAlarmService.EXTRA_SESSION_ID);
            final String sessionTitle = intent.getStringExtra(SessionAlarmService.EXTRA_SESSION_TITLE);
            if (sessionTitle == null || sessionEnd == UNDEFINED_VALUE || sessionId == null) {
                LOGE(TAG, "Attempted to schedule for feedback without providing extras.");
                return;
            }
            LOGD(TAG, "Scheduling feedback alarm for session: " + sessionTitle);
            scheduleFeedbackAlarm(sessionEnd, sessionAlarmOffset, sessionTitle);
            return;
        }

        final long sessionStart = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_START, UNDEFINED_VALUE);
        if (sessionStart == UNDEFINED_VALUE) {
            LOGD(TAG, "IGNORING ACTION -- no session start parameter.");
            return;
        }

        if (ACTION_NOTIFY_SESSION.equals(action)) {
            LOGD(TAG, "Notifying about sessions starting at " + sessionStart + " = "
                    + (new Date(sessionStart)).toString());
            LOGD(TAG, "-> Alarm offset: " + sessionAlarmOffset);
            notifySession(sessionStart, sessionAlarmOffset);
        } else if (ACTION_SCHEDULE_STARRED_BLOCK.equals(action)) {
            LOGD(TAG, "Scheduling session alarm.");
            LOGD(TAG, "-> Session start: " + sessionStart + " = " + (new Date(sessionStart)).toString());
            LOGD(TAG, "-> Session end: " + sessionEnd + " = " + (new Date(sessionEnd)).toString());
            LOGD(TAG, "-> Alarm offset: " + sessionAlarmOffset);
            scheduleAlarm(sessionStart, sessionEnd, sessionAlarmOffset);
        }
    }

    public void scheduleFeedbackAlarm(final long sessionEnd, final long alarmOffset, final String sessionTitle) {
        // By default, feedback alarms fire 5 minutes before session end time. If alarm offset is
        // provided, alarm is set to go off that much time from now (useful for testing).
        long alarmTime;
        if (alarmOffset == UNDEFINED_ALARM_OFFSET) {
            alarmTime = sessionEnd - MILLI_FIVE_MINUTES;
        } else {
            alarmTime = UIUtils.getCurrentTime(this) + alarmOffset;
        }

        LOGD(TAG, "Scheduling session feedback alarm for session '" + sessionTitle + "'");
        LOGD(TAG, "  -> end time: " + sessionEnd + " = " + (new Date(sessionEnd)).toString());
        LOGD(TAG, "  -> alarm time: " + alarmTime + " = " + (new Date(alarmTime)).toString());

        final Intent feedbackIntent = new Intent(ACTION_NOTIFY_SESSION_FEEDBACK, null, this,
                SessionAlarmService.class);
        PendingIntent pi = PendingIntent.getService(this, 1, feedbackIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        final AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        am.set(AlarmManager.RTC_WAKEUP, alarmTime, pi);
    }

    private void scheduleAlarm(final long sessionStart, final long sessionEnd, final long alarmOffset) {

        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(NOTIFICATION_ID);
        final long currentTime = UIUtils.getCurrentTime(this);
        // If the session is already started, do not schedule system notification.
        if (currentTime > sessionStart) {
            LOGD(TAG, "Not scheduling alarm because target time is in the past: " + sessionStart);
            return;
        }

        // By default, sets alarm to go off at 10 minutes before session start time.  If alarm
        // offset is provided, alarm is set to go off by that much time from now.
        long alarmTime;
        if (alarmOffset == UNDEFINED_ALARM_OFFSET) {
            alarmTime = sessionStart - MILLI_TEN_MINUTES;
        } else {
            alarmTime = currentTime + alarmOffset;
        }

        LOGD(TAG, "Scheduling alarm for " + alarmTime + " = " + (new Date(alarmTime)).toString());

        final Intent notifIntent = new Intent(ACTION_NOTIFY_SESSION, null, this, SessionAlarmService.class);
        // Setting data to ensure intent's uniqueness for different session start times.
        notifIntent.setData(new Uri.Builder().authority("com.google.samples.apps.iosched")
                .path(String.valueOf(sessionStart)).build());
        notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, sessionStart);
        LOGD(TAG, "-> Intent extra: session start " + sessionStart);
        notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, sessionEnd);
        LOGD(TAG, "-> Intent extra: session end " + sessionEnd);
        notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, alarmOffset);
        LOGD(TAG, "-> Intent extra: session alarm offset " + alarmOffset);
        PendingIntent pi = PendingIntent.getService(this, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        final AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        // Schedule an alarm to be fired to notify user of added sessions are about to begin.
        LOGD(TAG, "-> Scheduling RTC_WAKEUP alarm at " + alarmTime);
        am.set(AlarmManager.RTC_WAKEUP, alarmTime, pi);
    }

    /**
     *  A starred session is about to end. Notify the user to provide session feedback.
     *  Constructs and triggers a system notification. Does nothing if the session has already
     *  concluded.
     */
    private void notifySessionFeedback(boolean debug) {
        LOGD(TAG, "Considering firing notification for session feedback.");

        if (debug) {
            LOGW(TAG, "Note: this is a debug notification.");
        }

        // Don't fire notification if this feature is disabled in settings
        if (!SettingsUtils.shouldShowSessionFeedbackReminders(this)) {
            LOGD(TAG, "Skipping session feedback notification. Disabled in settings.");
            return;
        }

        Cursor c = null;
        try {
            c = getContentResolver().query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI,
                    SessionsNeedingFeedbackQuery.PROJECTION, SessionsNeedingFeedbackQuery.WHERE_CLAUSE, null, null);
            if (c == null) {
                return;
            }

            FeedbackHelper feedbackHelper = new FeedbackHelper(this);

            List<String> needFeedbackIds = new ArrayList<String>();
            List<String> needFeedbackTitles = new ArrayList<String>();
            while (c.moveToNext()) {
                String sessionId = c.getString(SessionsNeedingFeedbackQuery.SESSION_ID);
                String sessionTitle = c.getString(SessionsNeedingFeedbackQuery.SESSION_TITLE);

                // Avoid repeated notifications.
                if (feedbackHelper.isFeedbackNotificationFiredForSession(sessionId)) {
                    LOGD(TAG, "Skipping repeated session feedback notification for session '" + sessionTitle + "'");
                    continue;
                }

                needFeedbackIds.add(sessionId);
                needFeedbackTitles.add(sessionTitle);
            }

            if (needFeedbackIds.size() == 0) {
                // the user has already been notified of all sessions needing feedback
                return;
            }

            LOGD(TAG, "Going forward with session feedback notification for " + needFeedbackIds.size()
                    + " session(s).");

            final Resources res = getResources();

            // this is used to synchronize deletion of notifications on phone and wear
            Intent dismissalIntent = new Intent(ACTION_NOTIFICATION_DISMISSAL);
            // TODO: fix Wear dismiss integration
            //dismissalIntent.putExtra(KEY_SESSION_ID, sessionId);
            PendingIntent dismissalPendingIntent = PendingIntent.getService(this, (int) new Date().getTime(),
                    dismissalIntent, PendingIntent.FLAG_UPDATE_CURRENT);

            String provideFeedbackTicker = res.getString(R.string.session_feedback_notification_ticker);
            NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this)
                    .setColor(getResources().getColor(R.color.theme_primary)).setContentText(provideFeedbackTicker)
                    .setTicker(provideFeedbackTicker)
                    .setLights(SessionAlarmService.NOTIFICATION_ARGB_COLOR,
                            SessionAlarmService.NOTIFICATION_LED_ON_MS, SessionAlarmService.NOTIFICATION_LED_OFF_MS)
                    .setSmallIcon(R.drawable.ic_stat_notification).setPriority(Notification.PRIORITY_LOW)
                    .setLocalOnly(true) // make it local to the phone
                    .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
                    .setDeleteIntent(dismissalPendingIntent).setAutoCancel(true);

            if (needFeedbackIds.size() == 1) {
                // Only 1 session needs feedback
                Uri sessionUri = ScheduleContract.Sessions.buildSessionUri(needFeedbackIds.get(0));
                PendingIntent pi = TaskStackBuilder.create(this)
                        .addNextIntent(new Intent(this, MyScheduleActivity.class))
                        .addNextIntent(
                                new Intent(Intent.ACTION_VIEW, sessionUri, this, SessionFeedbackActivity.class))
                        .getPendingIntent(1, PendingIntent.FLAG_CANCEL_CURRENT);

                notifBuilder.setContentTitle(needFeedbackTitles.get(0)).setContentIntent(pi);
            } else {
                // Show information about several sessions that need feedback
                PendingIntent pi = TaskStackBuilder.create(this)
                        .addNextIntent(new Intent(this, MyScheduleActivity.class))
                        .getPendingIntent(1, PendingIntent.FLAG_CANCEL_CURRENT);

                NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
                inboxStyle.setBigContentTitle(provideFeedbackTicker);
                for (String title : needFeedbackTitles) {
                    inboxStyle.addLine(title);
                }

                notifBuilder
                        .setContentTitle(getResources().getQuantityString(R.plurals.session_plurals,
                                needFeedbackIds.size(), needFeedbackIds.size()))
                        .setStyle(inboxStyle).setContentIntent(pi);
            }

            NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            LOGD(TAG, "Now showing session feedback notification!");
            nm.notify(FEEDBACK_NOTIFICATION_ID, notifBuilder.build());

            for (int i = 0; i < needFeedbackIds.size(); i++) {
                setupNotificationOnWear(needFeedbackIds.get(i), null, needFeedbackTitles.get(i), null);
                feedbackHelper.setFeedbackNotificationAsFiredForSession(needFeedbackIds.get(i));
            }
        } finally {
            if (c != null) {
                try {
                    c.close();
                } catch (Exception ignored) {
                }
            }
        }
    }

    /**
     * Builds corresponding notification for the Wear device that is paired to this handset. This
     * is done by adding a Data Item to teh Data Store; the Wear device will be notified to build a
     * local notification.
     */
    private void setupNotificationOnWear(String sessionId, String sessionRoom, String sessionName, String speaker) {
        if (!mGoogleApiClient.isConnected()) {
            Log.e(TAG, "setupNotificationOnWear(): Failed to send data item since there was no "
                    + "connectivity to Google API Client");
            return;
        }
        PutDataMapRequest putDataMapRequest = PutDataMapRequest
                .create(FeedbackHelper.getFeedbackDataPathForWear(sessionId));
        putDataMapRequest.getDataMap().putLong("time", new Date().getTime());
        putDataMapRequest.getDataMap().putString(KEY_SESSION_ID, sessionId);
        putDataMapRequest.getDataMap().putString(KEY_SESSION_NAME, sessionName);
        putDataMapRequest.getDataMap().putString(KEY_SPEAKER_NAME, speaker);
        putDataMapRequest.getDataMap().putString(KEY_SESSION_ROOM, sessionRoom);

        PutDataRequest request = putDataMapRequest.asPutDataRequest();

        Wearable.DataApi.putDataItem(mGoogleApiClient, request)
                .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
                    @Override
                    public void onResult(DataApi.DataItemResult dataItemResult) {
                        LOGD(TAG, "setupNotificationOnWear(): Sending notification result success:"
                                + dataItemResult.getStatus().isSuccess());
                    }
                });
    }

    // Starred sessions are about to begin.  Constructs and triggers system notification.
    private void notifySession(final long sessionStart, final long alarmOffset) {
        long currentTime = UIUtils.getCurrentTime(this);
        final long intervalEnd = sessionStart + MILLI_TEN_MINUTES;
        LOGD(TAG, "Considering notifying for time interval.");
        LOGD(TAG, "    Interval start: " + sessionStart + "=" + (new Date(sessionStart)).toString());
        LOGD(TAG, "    Interval end: " + intervalEnd + "=" + (new Date(intervalEnd)).toString());
        LOGD(TAG, "    Current time is: " + currentTime + "=" + (new Date(currentTime)).toString());
        if (sessionStart < currentTime) {
            LOGD(TAG, "Skipping session notification (too late -- time interval already started)");
            return;
        }

        if (!SettingsUtils.shouldShowSessionReminders(this)) {
            // skip if disabled in settings
            LOGD(TAG, "Skipping session notification for sessions. Disabled in settings.");
            return;
        }

        // Avoid repeated notifications.
        if (alarmOffset == UNDEFINED_ALARM_OFFSET && UIUtils.isNotificationFiredForBlock(this,
                ScheduleContract.Blocks.generateBlockId(sessionStart, intervalEnd))) {
            LOGD(TAG, "Skipping session notification (already notified)");
            return;
        }

        final ContentResolver cr = getContentResolver();

        LOGD(TAG, "Looking for sessions in interval " + sessionStart + " - " + intervalEnd);
        Cursor c = null;
        try {
            c = cr.query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI, SessionDetailQuery.PROJECTION,
                    ScheduleContract.Sessions.STARTING_AT_TIME_INTERVAL_SELECTION,
                    ScheduleContract.Sessions.buildAtTimeIntervalArgs(sessionStart, intervalEnd), null);
            int starredCount = c.getCount();
            LOGD(TAG, "# starred sessions in that interval: " + c.getCount());
            String singleSessionId = null;
            String singleSessionRoomId = null;
            ArrayList<String> starredSessionTitles = new ArrayList<String>();
            while (c.moveToNext()) {
                singleSessionId = c.getString(SessionDetailQuery.SESSION_ID);
                singleSessionRoomId = c.getString(SessionDetailQuery.ROOM_ID);
                starredSessionTitles.add(c.getString(SessionDetailQuery.SESSION_TITLE));
                LOGD(TAG, "-> Title: " + c.getString(SessionDetailQuery.SESSION_TITLE));
            }
            if (starredCount < 1) {
                return;
            }

            // Generates the pending intent which gets fired when the user taps on the notification.
            // NOTE: Use TaskStackBuilder to comply with Android's design guidelines
            // related to navigation from notifications.
            Intent baseIntent = new Intent(this, MyScheduleActivity.class);
            baseIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
            TaskStackBuilder taskBuilder = TaskStackBuilder.create(this).addNextIntent(baseIntent);

            // For a single session, tapping the notification should open the session details (b/15350787)
            if (starredCount == 1) {
                taskBuilder.addNextIntent(
                        new Intent(Intent.ACTION_VIEW, ScheduleContract.Sessions.buildSessionUri(singleSessionId)));
            }

            PendingIntent pi = taskBuilder.getPendingIntent(0, PendingIntent.FLAG_CANCEL_CURRENT);

            final Resources res = getResources();
            String contentText;
            int minutesLeft = (int) (sessionStart - currentTime + 59000) / 60000;
            if (minutesLeft < 1) {
                minutesLeft = 1;
            }

            if (starredCount == 1) {
                contentText = res.getString(R.string.session_notification_text_1, minutesLeft);
            } else {
                contentText = res.getQuantityString(R.plurals.session_notification_text, starredCount - 1,
                        minutesLeft, starredCount - 1);
            }

            NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this)
                    .setContentTitle(starredSessionTitles.get(0)).setContentText(contentText)
                    .setColor(getResources().getColor(R.color.theme_primary))
                    .setTicker(res
                            .getQuantityString(R.plurals.session_notification_ticker, starredCount, starredCount))
                    .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE)
                    .setLights(SessionAlarmService.NOTIFICATION_ARGB_COLOR,
                            SessionAlarmService.NOTIFICATION_LED_ON_MS, SessionAlarmService.NOTIFICATION_LED_OFF_MS)
                    .setSmallIcon(R.drawable.ic_stat_notification).setContentIntent(pi)
                    .setPriority(Notification.PRIORITY_MAX).setAutoCancel(true);
            if (minutesLeft > 5) {
                notifBuilder.addAction(R.drawable.ic_alarm_holo_dark,
                        String.format(res.getString(R.string.snooze_x_min), 5),
                        createSnoozeIntent(sessionStart, intervalEnd, 5));
            }
            if (starredCount == 1 && SettingsUtils.isAttendeeAtVenue(this)) {
                notifBuilder.addAction(R.drawable.ic_map_holo_dark, res.getString(R.string.title_map),
                        createRoomMapIntent(singleSessionRoomId));
            }
            String bigContentTitle;
            if (starredCount == 1 && starredSessionTitles.size() > 0) {
                bigContentTitle = starredSessionTitles.get(0);
            } else {
                bigContentTitle = res.getQuantityString(R.plurals.session_notification_title, starredCount,
                        minutesLeft, starredCount);
            }
            NotificationCompat.InboxStyle richNotification = new NotificationCompat.InboxStyle(notifBuilder)
                    .setBigContentTitle(bigContentTitle);

            // Adds starred sessions starting at this time block to the notification.
            for (int i = 0; i < starredCount; i++) {
                richNotification.addLine(starredSessionTitles.get(i));
            }
            NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            LOGD(TAG, "Now showing notification.");
            nm.notify(NOTIFICATION_ID, richNotification.build());
        } finally {
            if (c != null) {
                try {
                    c.close();
                } catch (Exception ignored) {
                }
            }
        }
    }

    private PendingIntent createSnoozeIntent(final long sessionStart, final long sessionEnd,
            final int snoozeMinutes) {
        Intent scheduleIntent = new Intent(SessionAlarmService.ACTION_SCHEDULE_STARRED_BLOCK, null, this,
                SessionAlarmService.class);
        scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, sessionStart);
        scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, sessionEnd);
        scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, snoozeMinutes * MILLI_ONE_MINUTE);
        return PendingIntent.getService(this, 0, scheduleIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    }

    private PendingIntent createRoomMapIntent(final String roomId) {
        Intent mapIntent = new Intent(getApplicationContext(), MapActivity.class);
        mapIntent.putExtra(MapActivity.EXTRA_ROOM, roomId);
        mapIntent.putExtra(MapActivity.EXTRA_DETACHED_MODE, true);
        return TaskStackBuilder.create(getApplicationContext())
                .addNextIntent(new Intent(this, ExploreIOActivity.class)).addNextIntent(mapIntent)
                .getPendingIntent(0, PendingIntent.FLAG_CANCEL_CURRENT);
    }

    private void scheduleAllStarredBlocks() {
        final ContentResolver cr = getContentResolver();
        Cursor c = null;
        try {
            c = cr.query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI,
                    new String[] {
                            ScheduleContractHelper.formatQueryDistinctParameter(
                                    ScheduleContract.Sessions.SESSION_START),
                            ScheduleContract.Sessions.SESSION_END,
                            ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE },
                    null, null, null);
            if (c == null) {
                return;
            }

            while (c.moveToNext()) {
                final long sessionStart = c.getLong(0);
                final long sessionEnd = c.getLong(1);
                scheduleAlarm(sessionStart, sessionEnd, UNDEFINED_ALARM_OFFSET);
            }
        } finally {
            if (c != null) {
                try {
                    c.close();
                } catch (Exception ignored) {
                }
            }
        }
    }

    // Schedules feedback alarms for all starred sessions.
    private void scheduleAllStarredSessionFeedbacks() {
        final ContentResolver cr = getContentResolver();
        Cursor c = null;
        try {
            c = cr.query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI,
                    new String[] { ScheduleContract.Sessions.SESSION_TITLE, ScheduleContract.Sessions.SESSION_END,
                            ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE, },
                    null, null, null);
            if (c == null) {
                return;
            }
            while (c.moveToNext()) {
                final String sessionTitle = c.getString(0);
                final long sessionEnd = c.getLong(1);
                scheduleFeedbackAlarm(sessionEnd, UNDEFINED_ALARM_OFFSET, sessionTitle);
            }
        } finally {
            if (c != null) {
                try {
                    c.close();
                } catch (Exception ignored) {
                }
            }
        }
    }

    public interface SessionDetailQuery {

        String[] PROJECTION = { ScheduleContract.Sessions.SESSION_ID, ScheduleContract.Sessions.SESSION_TITLE,
                ScheduleContract.Sessions.ROOM_ID, ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE };

        int SESSION_ID = 0;
        int SESSION_TITLE = 1;
        int ROOM_ID = 2;
    }

    public interface SessionsNeedingFeedbackQuery {
        String[] PROJECTION = { ScheduleContract.Sessions.SESSION_ID, ScheduleContract.Sessions.SESSION_TITLE,
                ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE, ScheduleContract.Sessions.HAS_GIVEN_FEEDBACK, };

        int SESSION_ID = 0;
        int SESSION_TITLE = 1;

        public static final String WHERE_CLAUSE = ScheduleContract.Sessions.HAS_GIVEN_FEEDBACK + "=0";
    }

    @Override
    public void onConnected(Bundle connectionHint) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Connected to Google Api Service");
        }
    }

    @Override
    public void onConnectionSuspended(int cause) {
        // Ignore
    }

    @Override
    public void onConnectionFailed(ConnectionResult result) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Disconnected from Google Api Service");
        }
    }

}