/*******************************************************************************
* Copyright (c) 2010 Denis Solonenko.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v2.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* Denis Solonenko - initial API and implementation
******************************************************************************/
package ru.orangesoftware.financisto2.service;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import ru.orangesoftware.financisto2.db.DatabaseAdapter;
import ru.orangesoftware.financisto2.model.RestoredTransaction;
import ru.orangesoftware.financisto2.model.SystemAttribute;
import ru.orangesoftware.financisto2.model.TransactionAttributeInfo;
import ru.orangesoftware.financisto2.model.TransactionInfo;
import ru.orangesoftware.financisto2.recur.DateRecurrenceIterator;
import ru.orangesoftware.financisto2.recur.Recurrence;
import ru.orangesoftware.financisto2.utils.MyPreferences;
@EBean(scope = EBean.Scope.Singleton)
public class RecurrenceScheduler {
private static final String TAG = "RecurrenceScheduler";
private static final Date NULL_DATE = new Date(0);
private static final int MAX_RESTORED = 1000;
public static final String SCHEDULED_TRANSACTION_ID = "scheduledTransactionId";
@Bean
public DatabaseAdapter db;
public int scheduleAll(Context context) {
long now = System.currentTimeMillis();
int restoredTransactionsCount = 0;
if (MyPreferences.isRestoreMissedScheduledTransactions(context)) {
restoredTransactionsCount = restoreMissedSchedules(now);
// all transactions up to and including now has already been restored
now += 1000;
}
scheduleAll(context, now);
return restoredTransactionsCount;
}
public TransactionInfo scheduleOne(Context context, long scheduledTransactionId) {
Log.i(TAG, "Alarm for "+scheduledTransactionId+" received..");
TransactionInfo transaction = db.getTransactionInfo(scheduledTransactionId);
if (transaction != null) {
long transactionId = duplicateTransactionFromTemplate(transaction);
boolean hasBeenRescheduled = rescheduleTransaction(context, transaction);
if (!hasBeenRescheduled) {
deleteTransactionIfNeeded(transaction);
Log.i(TAG, "Expired transaction "+transaction.id+" has been deleted");
}
transaction.id = transactionId;
return transaction;
}
return null;
}
private void deleteTransactionIfNeeded(TransactionInfo transaction) {
TransactionAttributeInfo a = db.getSystemAttributeForTransaction(SystemAttribute.DELETE_AFTER_EXPIRED, transaction.id);
if (a != null && Boolean.valueOf(a.value)) {
db.deleteTransaction(transaction.id);
}
}
/**
* Restores missed scheduled transactions on backup and on phone restart
* @param now current time
* @return restored transactions count
*/
private int restoreMissedSchedules(long now) {
try {
List<RestoredTransaction> restored = getMissedSchedules(now);
if (restored.size() > 0) {
db.storeMissedSchedules(restored, now);
Log.i(TAG, "["+restored.size()+"] scheduled transactions have been restored:");
for (int i=0; i<10 && i<restored.size(); i++) {
RestoredTransaction rt = restored.get(i);
Log.i(TAG, rt.transactionId+" at "+rt.dateTime);
}
return restored.size();
}
} catch (Exception ex) {
// eat all exceptions
Log.e(TAG, "Unexpected error while restoring schedules", ex);
}
return 0;
}
private long duplicateTransactionFromTemplate(TransactionInfo transaction) {
return db.duplicateTransaction(transaction.id);
}
public List<RestoredTransaction> getMissedSchedules(long now) {
long t0 = System.currentTimeMillis();
try {
Date endDate = new Date(now);
List<RestoredTransaction> restored = new ArrayList<RestoredTransaction>();
ArrayList<TransactionInfo> list = db.getAllScheduledTransactions();
for (TransactionInfo t : list) {
if (t.recurrence != null) {
long lastRecurrence = t.lastRecurrence;
if (lastRecurrence > 0) {
// move lastRecurrence time by 1 sec into future to not trigger the same time again
DateRecurrenceIterator ri = createIterator(t.recurrence, lastRecurrence+1000);
while (ri.hasNext()) {
Date nextDate = ri.next();
if (nextDate.after(endDate)) {
break;
}
addRestoredTransaction(restored, t, nextDate);
}
}
} else {
Date nextDate = new Date(t.dateTime);
if (nextDate.before(endDate)) {
addRestoredTransaction(restored, t, nextDate);
}
}
}
if (restored.size() > MAX_RESTORED) {
Collections.sort(restored, new Comparator<RestoredTransaction>(){
@Override
public int compare(RestoredTransaction t0, RestoredTransaction t1) {
return t1.dateTime.compareTo(t0.dateTime);
}
});
restored = restored.subList(0, MAX_RESTORED);
}
return restored;
} finally {
Log.i(TAG, "getSortedSchedules="+(System.currentTimeMillis()-t0)+"ms");
}
}
private void addRestoredTransaction(List<RestoredTransaction> restored,
TransactionInfo t, Date nextDate) {
RestoredTransaction rt = new RestoredTransaction(t.id, nextDate);
restored.add(rt);
}
public ArrayList<TransactionInfo> getSortedSchedules(long now) {
long t0 = System.currentTimeMillis();
try {
ArrayList<TransactionInfo> list = db.getAllScheduledTransactions();
Log.i(TAG, "Got "+list.size()+" scheduled transactions");
calculateNextScheduleDateForAllTransactions(list, now);
sortTransactionsByScheduleDate(list, now);
return list;
} finally {
Log.i(TAG, "getSortedSchedules="+(System.currentTimeMillis()-t0)+"ms");
}
}
public ArrayList<TransactionInfo> scheduleAll(Context context, long now) {
ArrayList<TransactionInfo> scheduled = getSortedSchedules(now);
for (TransactionInfo transaction : scheduled) {
scheduleAlarm(context, transaction, now);
}
return scheduled;
}
public boolean scheduleAlarm(Context context, TransactionInfo transaction, long now) {
if (shouldSchedule(transaction, now)) {
Date scheduleTime = transaction.nextDateTime;
AlarmManager service = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = createPendingIntentForScheduledAlarm(context, transaction.id);
service.set(AlarmManager.RTC_WAKEUP, scheduleTime.getTime(), pendingIntent);
Log.i(TAG, "Scheduling alarm for "+transaction.id+" at "+scheduleTime);
return true;
}
Log.i(TAG, "Transactions "+transaction.id+" with next date/time "+transaction.nextDateTime+" is not selected for schedule");
return false;
}
public boolean rescheduleTransaction(Context context, TransactionInfo transaction) {
if (transaction.recurrence != null) {
long now = System.currentTimeMillis()+1000;
calculateAndSetNextDateTimeOnTransaction(transaction, now);
return scheduleAlarm(context, transaction, now);
}
return false;
}
private boolean shouldSchedule(TransactionInfo transaction, long now) {
return transaction.nextDateTime != null && now < transaction.nextDateTime.getTime();
}
public void cancelPendingIntentForSchedule(Context context, long transactionId) {
Log.i(TAG, "Cancelling pending alarm for "+transactionId);
AlarmManager service = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
PendingIntent intent = createPendingIntentForScheduledAlarm(context, transactionId);
service.cancel(intent);
}
private PendingIntent createPendingIntentForScheduledAlarm(Context context, long transactionId) {
Intent intent = new Intent("ru.orangesoftware.financisto2.SCHEDULED_ALARM");
intent.putExtra(SCHEDULED_TRANSACTION_ID, transactionId);
return PendingIntent.getBroadcast(context, (int)transactionId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
/**
* Correct order by nextDateTime:
* 2010-12-01
* 2010-12-02
* 2010-11-23 <- today
* 2010-11-11
* 2010-10-08
* NULL
*/
public static class RecurrenceComparator implements Comparator<TransactionInfo> {
private final Date today;
public RecurrenceComparator(long now) {
this.today = new Date(now);
}
@Override
public int compare(TransactionInfo o1, TransactionInfo o2) {
Date d1 = o1 != null ? (o1.nextDateTime != null ? o1.nextDateTime : NULL_DATE) : NULL_DATE;
Date d2 = o2 != null ? (o2.nextDateTime != null ? o2.nextDateTime : NULL_DATE) : NULL_DATE;
if (d1.after(today)) {
if (d2.after(today)) {
return d1.compareTo(d2);
} else {
return -1;
}
} else {
if (d2.after(today)) {
return 1;
} else {
return -d1.compareTo(d2);
}
}
}
}
private void sortTransactionsByScheduleDate(ArrayList<TransactionInfo> list, long now) {
Collections.sort(list, new RecurrenceComparator(now));
}
private long calculateNextScheduleDateForAllTransactions(ArrayList<TransactionInfo> list, long now) {
for (TransactionInfo t : list) {
calculateAndSetNextDateTimeOnTransaction(t, now);
}
return now;
}
private void calculateAndSetNextDateTimeOnTransaction(TransactionInfo t, long now) {
if (t.recurrence != null) {
t.nextDateTime = calculateNextDate(t.recurrence, now);
} else {
t.nextDateTime = new Date(t.dateTime);
}
Log.i(TAG, "Calculated schedule time for "+t.id+" is "+t.nextDateTime);
}
public Date calculateNextDate(String recurrence, long now) {
try {
DateRecurrenceIterator ri = createIterator(recurrence, now);
if (ri.hasNext()) {
return ri.next();
}
} catch (Exception ex) {
Log.e("Financisto", "Unable to calculate next date for "+recurrence+" at "+now);
}
return null;
}
private DateRecurrenceIterator createIterator(String recurrence, long now) {
Recurrence r = Recurrence.parse(recurrence);
Date advanceDate = new Date(now);
return r.createIterator(advanceDate);
}
}