/* * 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 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.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; 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.TimeUtils; import com.google.samples.apps.iosched.util.UIUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; 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 { 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"; 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 protected void onHandleIntent(Intent intent) { 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 = TimeUtils.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 = TimeUtils.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(); Intent dismissalIntent = new Intent(ACTION_NOTIFICATION_DISMISSAL); 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); //noinspection deprecation Ignore getColor deprecation until minSdk = 16 notifBuilder.setColor(getResources().getColor(R.color.theme_primary)); notifBuilder.setContentText(provideFeedbackTicker) .setTicker(provideFeedbackTicker) .setSmallIcon(R.drawable.ic_stat_notification) .setPriority(Notification.PRIORITY_LOW) .setLocalOnly(true) // make it local to the phone .setDeleteIntent(dismissalPendingIntent) .setAutoCancel(true); // Note, this notification doesn't warrant forced vibration or notification lights per // review. 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++) { feedbackHelper.setFeedbackNotificationAsFiredForSession(needFeedbackIds.get(i)); } } finally { if (c != null) { try { c.close(); } catch (Exception ignored) { } } } } // Starred sessions are about to begin. Constructs and triggers system notification. private void notifySession(final long sessionStart, final long alarmOffset) { long currentTime = TimeUtils.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_stat_alarm, 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_stat_map, 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"; } }