package be.digitalia.fosdem.services;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.StyleSpan;
import be.digitalia.fosdem.BuildConfig;
import be.digitalia.fosdem.R;
import be.digitalia.fosdem.activities.EventDetailsActivity;
import be.digitalia.fosdem.activities.MainActivity;
import be.digitalia.fosdem.activities.RoomImageDialogActivity;
import be.digitalia.fosdem.activities.SettingsActivity;
import be.digitalia.fosdem.db.DatabaseManager;
import be.digitalia.fosdem.model.Event;
import be.digitalia.fosdem.receivers.AlarmReceiver;
import be.digitalia.fosdem.utils.StringUtils;
/**
* A service to schedule or unschedule alarms in the background, keeping the app responsive.
*
* @author Christophe Beyls
*/
public class AlarmIntentService extends IntentService {
public static final String ACTION_UPDATE_ALARMS = BuildConfig.APPLICATION_ID + ".action.UPDATE_ALARMS";
public static final String EXTRA_WITH_WAKE_LOCK = "with_wake_lock";
public static final String ACTION_DISABLE_ALARMS = BuildConfig.APPLICATION_ID + ".action.DISABLE_ALARMS";
private AlarmManager alarmManager;
public AlarmIntentService() {
super("AlarmIntentService");
}
@Override
public void onCreate() {
super.onCreate();
// Ask for the last unhandled intents to be redelivered if the service dies early.
// This ensures we handle all events, in order.
setIntentRedelivery(true);
alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
}
private PendingIntent getAlarmPendingIntent(long eventId) {
Intent intent = new Intent(this, AlarmReceiver.class)
.setAction(AlarmReceiver.ACTION_NOTIFY_EVENT)
.setData(Uri.parse(String.valueOf(eventId)));
return PendingIntent.getBroadcast(this, 0, intent, 0);
}
@Override
protected void onHandleIntent(Intent intent) {
switch (intent.getAction()) {
case ACTION_UPDATE_ALARMS: {
// Create/update all alarms
final long delay = getDelay();
final long now = System.currentTimeMillis();
boolean hasAlarms = false;
Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
try {
while (cursor.moveToNext()) {
long eventId = DatabaseManager.toEventId(cursor);
long notificationTime = DatabaseManager.toEventStartTimeMillis(cursor) - delay;
PendingIntent pi = getAlarmPendingIntent(eventId);
if (notificationTime < now) {
// Cancel pending alarms that are now scheduled in the past, if any
alarmManager.cancel(pi);
} else {
setExactAlarm(alarmManager, AlarmManager.RTC_WAKEUP, notificationTime, pi);
hasAlarms = true;
}
}
} finally {
cursor.close();
}
setAlarmReceiverEnabled(hasAlarms);
break;
}
case ACTION_DISABLE_ALARMS: {
// Cancel alarms of every bookmark in the future
Cursor cursor = DatabaseManager.getInstance().getBookmarks(System.currentTimeMillis());
try {
while (cursor.moveToNext()) {
long eventId = DatabaseManager.toEventId(cursor);
alarmManager.cancel(getAlarmPendingIntent(eventId));
}
} finally {
cursor.close();
}
setAlarmReceiverEnabled(false);
break;
}
case DatabaseManager.ACTION_ADD_BOOKMARK: {
long delay = getDelay();
long eventId = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L);
long startTime = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_START_TIME, -1L);
// Only schedule future events. If they start before the delay, the alarm will go off immediately
if ((startTime == -1L) || (startTime < System.currentTimeMillis())) {
break;
}
setAlarmReceiverEnabled(true);
setExactAlarm(alarmManager, AlarmManager.RTC_WAKEUP, startTime - delay, getAlarmPendingIntent(eventId));
break;
}
case DatabaseManager.ACTION_REMOVE_BOOKMARKS: {
// Cancel matching alarms, might they exist or not
long[] eventIds = intent.getLongArrayExtra(DatabaseManager.EXTRA_EVENT_IDS);
for (long eventId : eventIds) {
alarmManager.cancel(getAlarmPendingIntent(eventId));
}
break;
}
case AlarmReceiver.ACTION_NOTIFY_EVENT: {
long eventId = Long.parseLong(intent.getDataString());
Event event = DatabaseManager.getInstance().getEvent(eventId);
if (event != null) {
PendingIntent eventPendingIntent = TaskStackBuilder
.create(this)
.addNextIntent(new Intent(this, MainActivity.class))
.addNextIntent(
new Intent(this, EventDetailsActivity.class).setData(Uri.parse(String.valueOf(event
.getId())))).getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
int defaultFlags = Notification.DEFAULT_SOUND;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(SettingsActivity.KEY_PREF_NOTIFICATIONS_VIBRATE, false)) {
defaultFlags |= Notification.DEFAULT_VIBRATE;
}
String personsSummary = event.getPersonsSummary();
String trackName = event.getTrack().getName();
String contentText;
CharSequence bigText;
if (TextUtils.isEmpty(personsSummary)) {
contentText = trackName;
bigText = event.getSubTitle();
} else {
contentText = String.format("%1$s - %2$s", trackName, personsSummary);
String subTitle = event.getSubTitle();
SpannableString spannableBigText;
if (TextUtils.isEmpty(subTitle)) {
spannableBigText = new SpannableString(personsSummary);
} else {
spannableBigText = new SpannableString(String.format("%1$s\n%2$s", subTitle, personsSummary));
}
// Set the persons summary in italic
spannableBigText.setSpan(new StyleSpan(Typeface.ITALIC),
spannableBigText.length() - personsSummary.length(), spannableBigText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
bigText = spannableBigText;
}
int notificationColor = ContextCompat.getColor(this, R.color.color_primary);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_stat_fosdem)
.setColor(notificationColor)
.setWhen(event.getStartTime().getTime())
.setContentTitle(event.getTitle())
.setContentText(contentText)
.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText).setSummaryText(trackName))
.setContentInfo(event.getRoomName())
.setContentIntent(eventPendingIntent)
.setAutoCancel(true)
.setDefaults(defaultFlags)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_EVENT);
// Blink the LED with FOSDEM color if enabled in the options
if (sharedPreferences.getBoolean(SettingsActivity.KEY_PREF_NOTIFICATIONS_LED, false)) {
notificationBuilder.setLights(notificationColor, 1000, 5000);
}
// Android Wear extensions
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender();
// Add an optional action button to show the room map image
String roomName = event.getRoomName();
int roomImageResId = getResources().getIdentifier(StringUtils.roomNameToResourceName(roomName),
"drawable", getPackageName());
if (roomImageResId != 0) {
// The room name is the unique Id of a RoomImageDialogActivity
Intent mapIntent = new Intent(this, RoomImageDialogActivity.class).setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK).setData(Uri.parse(roomName));
mapIntent.putExtra(RoomImageDialogActivity.EXTRA_ROOM_NAME, roomName);
mapIntent.putExtra(RoomImageDialogActivity.EXTRA_ROOM_IMAGE_RESOURCE_ID, roomImageResId);
PendingIntent mapPendingIntent = PendingIntent.getActivity(this, 0, mapIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
CharSequence mapTitle = getString(R.string.room_map);
notificationBuilder.addAction(new NotificationCompat.Action(R.drawable.ic_place_white_24dp, mapTitle,
mapPendingIntent));
// Use bigger action icon for wearable notification
wearableExtender.addAction(new NotificationCompat.Action(R.drawable.ic_place_white_wear, mapTitle,
mapPendingIntent));
}
notificationBuilder.extend(wearableExtender);
NotificationManagerCompat.from(this).notify((int) eventId, notificationBuilder.build());
}
break;
}
}
// Release the wake lock setup by AlarmReceiver, if any
if (intent.getBooleanExtra(EXTRA_WITH_WAKE_LOCK, false)) {
AlarmReceiver.completeWakefulIntent(intent);
}
}
private static void setExactAlarm(AlarmManager manager, int type, long triggerAtMillis, PendingIntent operation) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
manager.setExact(type, triggerAtMillis, operation);
} else {
manager.set(type, triggerAtMillis, operation);
}
}
private long getDelay() {
String delayString = PreferenceManager.getDefaultSharedPreferences(this).getString(
SettingsActivity.KEY_PREF_NOTIFICATIONS_DELAY, "0");
// Convert from minutes to milliseconds
return Long.parseLong(delayString) * DateUtils.MINUTE_IN_MILLIS;
}
/**
* Allows disabling the Alarm Receiver so the app is not loaded at boot when it's not necessary.
*/
private void setAlarmReceiverEnabled(boolean isEnabled) {
ComponentName componentName = new ComponentName(this, AlarmReceiver.class);
int flag = isEnabled ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
getPackageManager().setComponentEnabledSetting(componentName, flag, PackageManager.DONT_KILL_APP);
}
}