/* * Copyright 2012 Google Inc. * * 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.android.apps.iosched.calendar; import com.google.android.apps.iosched.R; import com.google.android.apps.iosched.provider.ScheduleContract; import com.google.android.apps.iosched.util.UIUtils; import android.annotation.TargetApi; 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.Build; import android.support.v4.app.NotificationCompat; import java.util.ArrayList; import static com.google.android.apps.iosched.util.LogUtils.makeLogTag; /** * Background service to handle scheduling of starred session notification via * {@link android.app.AlarmManager}. */ public class SessionAlarmService extends IntentService { private static final String TAG = makeLogTag(SessionAlarmService.class); public static final String ACTION_NOTIFY_SESSION = "com.google.android.apps.iosched.action.NOTIFY_SESSION"; public static final String ACTION_SCHEDULE_STARRED_BLOCK = "com.google.android.apps.iosched.action.SCHEDULE_STARRED_BLOCK"; public static final String ACTION_SCHEDULE_ALL_STARRED_BLOCKS = "com.google.android.apps.iosched.action.SCHEDULE_ALL_STARRED_BLOCKS"; public static final String EXTRA_SESSION_START = "com.google.android.apps.iosched.extra.SESSION_START"; public static final String EXTRA_SESSION_END = "com.google.android.apps.iosched.extra.SESSION_END"; public static final String EXTRA_SESSION_ALARM_OFFSET = "com.google.android.apps.iosched.extra.SESSION_ALARM_OFFSET"; private static final int NOTIFICATION_ID = 100; // 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 ONE_MINUTE_MILLIS = 1 * 60 * 1000; private static final long TEN_MINUTES_MILLIS = 10 * ONE_MINUTE_MILLIS; private static final long UNDEFINED_ALARM_OFFSET = -1; public SessionAlarmService() { super(TAG); } @Override protected void onHandleIntent(Intent intent) { final String action = intent.getAction(); if (ACTION_SCHEDULE_ALL_STARRED_BLOCKS.equals(action)) { scheduleAllStarredBlocks(); return; } final long sessionStart = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_START, -1); if (sessionStart == -1) { return; } final long sessionEnd = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_END, -1); if (sessionEnd == -1) { return; } final long sessionAlarmOffset = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, UNDEFINED_ALARM_OFFSET); if (ACTION_NOTIFY_SESSION.equals(action)) { notifySession(sessionStart, sessionEnd, sessionAlarmOffset); } else if (ACTION_SCHEDULE_STARRED_BLOCK.equals(action)) { scheduleAlarm(sessionStart, sessionEnd, sessionAlarmOffset); } } 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 = System.currentTimeMillis(); // If the session is already started, do not schedule system notification. if (currentTime > 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 - TEN_MINUTES_MILLIS; } else { alarmTime = currentTime + alarmOffset; } final Intent alarmIntent = new Intent( ACTION_NOTIFY_SESSION, null, this, SessionAlarmService.class); // Setting data to ensure intent's uniqueness for different session start times. alarmIntent.setData( new Uri.Builder() .authority(ScheduleContract.CONTENT_AUTHORITY) .path(String.valueOf(sessionStart)) .build()); alarmIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, sessionStart); alarmIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, sessionEnd); alarmIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, alarmOffset); final AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // Schedule an alarm to be fired to notify user of added sessions are about to begin. am.set(AlarmManager.RTC_WAKEUP, alarmTime, PendingIntent.getService(this, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT)); } /** * Constructs and triggers system notification for when starred sessions are about to begin. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void notifySession(final long sessionStart, final long sessionEnd, final long alarmOffset) { long currentTime = System.currentTimeMillis(); if (sessionStart < currentTime) { return; } // Avoid repeated notifications. if (alarmOffset == UNDEFINED_ALARM_OFFSET && UIUtils.isNotificationFiredForBlock( this, ScheduleContract.Blocks.generateBlockId(sessionStart, sessionEnd))) { return; } final ContentResolver resolver = getContentResolver(); final Uri starredBlockUri = ScheduleContract.Blocks.buildStarredSessionsUri( ScheduleContract.Blocks.generateBlockId(sessionStart, sessionEnd)); Cursor cursor = resolver.query(starredBlockUri, new String[]{ ScheduleContract.Blocks.NUM_STARRED_SESSIONS, ScheduleContract.Sessions.SESSION_TITLE }, null, null, null); int starredCount = 0; ArrayList<String> starredSessionTitles = new ArrayList<String>(); while (cursor.moveToNext()) { starredSessionTitles.add(cursor.getString(1)); starredCount = cursor.getInt(0); } if (starredCount < 1) { return; } // Generates the pending intent which gets fired when the user touches on the notification. Intent sessionIntent = new Intent(Intent.ACTION_VIEW, starredBlockUri); sessionIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pi = PendingIntent.getActivity(this, 0, sessionIntent, 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); } // Construct a notification. Use Jelly Bean (API 16) rich notifications if possible. Notification notification; if (UIUtils.hasJellyBean()) { // Rich notifications Notification.Builder builder = new Notification.Builder(this) .setContentTitle(starredSessionTitles.get(0)) .setContentText(contentText) .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) { builder.addAction(R.drawable.ic_alarm_holo_dark, String.format(res.getString(R.string.snooze_x_min), 5), createSnoozeIntent(sessionStart, sessionEnd, 5)); } Notification.InboxStyle richNotification = new Notification.InboxStyle( builder) .setBigContentTitle(res.getQuantityString(R.plurals.session_notification_title, starredCount, minutesLeft, starredCount)); // Adds starred sessions starting at this time block to the notification. for (int i = 0; i < starredCount; i++) { richNotification.addLine(starredSessionTitles.get(i)); } notification = richNotification.build(); } else { // Pre-Jelly Bean non-rich notifications notification = new NotificationCompat.Builder(this) .setContentTitle(starredSessionTitles.get(0)) .setContentText(contentText) .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) .getNotification(); } NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(NOTIFICATION_ID, notification); } 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 * ONE_MINUTE_MILLIS); return PendingIntent.getService(this, 0, scheduleIntent, PendingIntent.FLAG_CANCEL_CURRENT); } private void scheduleAllStarredBlocks() { final Cursor cursor = getContentResolver().query( ScheduleContract.Sessions.CONTENT_STARRED_URI, new String[] { "distinct " + ScheduleContract.Sessions.BLOCK_START, ScheduleContract.Sessions.BLOCK_END }, null, null, null); if (cursor == null) { return; } while (cursor.moveToNext()) { final long sessionStart = cursor.getLong(0); final long sessionEnd = cursor.getLong(1); scheduleAlarm(sessionStart, sessionEnd, UNDEFINED_ALARM_OFFSET); } } }