Android Alarm Manager Set Repeating at Specific Timing

user2424370 picture user2424370 · Oct 1, 2014 · Viewed 14k times · Source

I am having some problem with alarm manager in Android. So what I am trying to do is set the alarm to repeat to run the DB insertion every day around 12.01AM.

Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(System.currentTimeMillis());
    calendar.set(Calendar.HOUR_OF_DAY, 0 );
    calendar.set(Calendar.MINUTE, 1);
        notificationCount = notificationCount + 1;
        AlarmManager mgr = (AlarmManager) context
                .getSystemService(Context.ALARM_SERVICE);
        Intent notificationIntent = new Intent(context,
                ReminderAlarm.class);

        notificationIntent.putExtra("NotifyCount", notificationCount);
        PendingIntent pi = PendingIntent.getBroadcast(context,
                notificationCount, notificationIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP,
                calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pi);

So basically I've came up with these code. However, the alarm manager execute again after the minute I set it.

Let's say I run the apps on 01/10/2014 5.48PM. I wanted this to run the DB insertion when onReceive every day after I set it around 12.01AM only. But somehow, the alarm manager execute at 01/10/2014 5.49PM which is one minute after I set it and it stopped working.

I wonder which part I did wrongly.

Thanks in advance.

EDIT

Recurring class For this class, it will trigger the alarm manager everyday and pass the variables along to reminder alarm class for DB insertion.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.recurring);
    context = this;
    buildListView();
    if(!alarmInitialized(this)) { 
        scheduleAlarms(this); 
    }
}

// And the few methods you suggested to schedule the alarm
public static void scheduleAlarms(Context context) {
    Calendar calendar = Calendar.getInstance();
    if (hasRunnedToday(context)) { // if the alarm has run this day
        calendar.add(Calendar.DATE, 1); // schedule it to run again starting
                                        // tomorrow
    }

    long firstRunTime = calendar.getTimeInMillis();
    AlarmManager mgr = (AlarmManager) context
            .getSystemService(Context.ALARM_SERVICE);
    Intent notificationIntent = new Intent(context, ReminderAlarm.class);
    PendingIntent pi = PendingIntent.getActivity(context, 0,
            notificationIntent, 0);
    mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstRunTime,
            AlarmManager.INTERVAL_DAY, pi);

    ComponentName receiver = new ComponentName(context, BootReceiver.class);
    PackageManager pm = context.getPackageManager();

    pm.setComponentEnabledSetting(receiver,
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
            PackageManager.DONT_KILL_APP);
}

BootReceiver class

public void onReceive(Context context, Intent i) {
    if (i.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
        Recurring.scheduleAlarms(context);
    }
}

ReminderAlarm class Basically for this class it just grab the variable passed from Recurring class and execute the DB insertion. I did inserted some Toast.makeText to test if it is retrieving but no luck by testing it.

public class ReminderAlarm extends BroadcastReceiver {
private NotificationManager mNotificationManager;
private Notification notification;

@Override
public void onReceive(Context context, Intent intent) {
    String recurID = null;
    String recurStartDate = null;
    String currentDate = null;
    String description = null;
    String type = null;
    String amount = null;
    String categoryName = null;
    String frequencyStr = null;
    String nextPaymentDate = null;
    SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");

    DatabaseAdapter mDbHelper = new DatabaseAdapter(context);
    mDbHelper.createDatabase();
    mDbHelper.open();
    RecurringController rc = new RecurringController(mDbHelper.open());
    ArrayList<RecurringModel> recur_list = rc.getAllRecurring();

    // THIS PART TO GET DATA FROM DATABASE
    for (int i = 0; i < recur_list.size(); i++) {
        recurID = recur_list.get(i).getRecurringID();
        recurStartDate = recur_list.get(i).getRecurringStartDate();
        currentDate = dateFormat.format(new Date());
        description = recur_list.get(i).getRecurringDesc();
        type = recur_list.get(i).getRecurringType();
        amount = Float.toString(recur_list.get(i).getRecurringAmount());
        categoryName = recur_list.get(i).getCategoryID();
        frequencyStr = recur_list.get(i).getFrequency();

        Toast.makeText(context,
                    description, Toast.LENGTH_LONG)
                    .show();
        Toast.makeText(context,
                    recurStartDate Toast.LENGTH_LONG)
                    .show();

        Calendar cal = Calendar.getInstance();
        try {
            cal.setTime(dateFormat.parse(recurStartDate));
            if (frequencyStr.equals("Daily")) {
                cal.add(Calendar.DAY_OF_MONTH, 1);
                nextPaymentDate = dateFormat.format(cal.getTimeInMillis());
                cal.add(Calendar.DAY_OF_MONTH, -1);
            } else if (frequencyStr.equals("Weekly")) {
                cal.add(Calendar.WEEK_OF_YEAR, 1);
                nextPaymentDate = dateFormat.format(cal.getTimeInMillis());
                cal.add(Calendar.WEEK_OF_YEAR, -1);
            } else if (frequencyStr.equals("Monthly")) {
                cal.add(Calendar.MONTH, 1);
                nextPaymentDate = dateFormat.format(cal.getTimeInMillis());
                cal.add(Calendar.MONTH, -1);
            } else if (frequencyStr.equals("Yearly")) {
                cal.add(Calendar.YEAR, 1);
                nextPaymentDate = dateFormat.format(cal.getTimeInMillis());
                cal.add(Calendar.YEAR, -1);
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }

        // If dates match then execute the SQL statements
        if (currentDate.equals(nextPaymentDate)) {
            // mDbHelper.createDatabase();
            // mDbHelper.open();
            TransactionRecModel trm = new TransactionRecModel();
            CategoryController cc = new CategoryController(mDbHelper.open());

            trm.setDate(currentDate);
            trm.setTransDescription(description);
            trm.setType(type);
            trm.setAmount(Float.parseFloat(amount));

            // Get the categoryID based on categoryName
            String catID = cc.getCatIDByName(categoryName);
            trm.setCategory(catID);

            // Check if the recurring record exists before insert new
            // transaction record
            boolean recurExist = rc.checkRecurExist(recurStartDate,
                    description, catID);
            if (recurExist == true) {
                TransactionRecController trc = new TransactionRecController(
                        mDbHelper.open());
                // Check if the transaction record exists to prevent
                // duplication
                boolean moveNext = trc.checkTransExist(trm);
                if (moveNext == false) {

                    if (trc.addTransactionRec(trm)) {
                        // Update recurring start date after insertion of
                        // transaction
                        RecurringModel rm = new RecurringModel();
                        rm.setRecurringID(recurID);
                        rm.setRecurringStartDate(currentDate);

                        if (rc.updateRecurringDate(rm)) {
                            mNotificationManager = (NotificationManager) context
                                    .getSystemService(Context.NOTIFICATION_SERVICE);
                            PendingIntent contentIntent = PendingIntent
                                    .getActivity(
                                            context,
                                            Integer.parseInt(intent
                                                    .getExtras()
                                                    .get("NotifyCount")
                                                    .toString()),
                                            new Intent(), 0);
                            notification = new Notification(
                                    R.drawable.ic_launcher, "Notification",
                                    System.currentTimeMillis());
                            notification.setLatestEventInfo(context,
                                    description, nextPaymentDate,
                                    contentIntent);
                            mNotificationManager
                                    .notify(Integer.parseInt(intent
                                            .getExtras().get("NotifyCount")
                                            .toString()), notification);
                            mDbHelper.close();
                        }
                    }
                }
            }
            mDbHelper.close();
        }
    }
    mDbHelper.close();
    Recurring.updateAlarmLastRun(context);
}
}

I've added this part of codes in the part you suggested to schedule the alarm to call the BootReceiver class. Then from BootReceiver class, I will call back to the Recurring class and Reminder Alarm class:

ComponentName receiver = new ComponentName(context, BootReceiver.class);
    PackageManager pm = context.getPackageManager();

    pm.setComponentEnabledSetting(receiver,
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
            PackageManager.DONT_KILL_APP);

Answer

forcewill picture forcewill · Oct 9, 2014

The problem is in calendar.getTimeInMillis() in

mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP,
            calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pi);

The second argument to setInexactRepeating quoting the doc

triggerAtMillis time in milliseconds that the alarm should first go off, using the appropriate clock (depending on the alarm type). This is inexact: the alarm will not fire before this time, but there may be a delay of almost an entire alarm interval before the first invocation of the alarm.

Meaning it will run the first time aproximally one minute after you set it because of

calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 0 );
calendar.set(Calendar.MINUTE, 1);

If you wan't the first run of the alarm to be the next day do a calendar.add(Calendar. DATE, 1);`

As to the it stopped working, did you reboot de device ? AlarmCalendar alarms don't persist to device reboot, you can register a BroadcastReceiver to receive BOOT_COMPLETED event and register the alarm again check does Alarm Manager persist even after reboot?

Update: as you requested here is some help after reviewing your code

In your BOOT_COMPLETED Receiver class:

public void onReceive(Context context, Intent i) {
    if (i.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
        ReminderAlarm.scheduleAlarms(this);
    }
}

In your ReminderAlarm class

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recurring);

if(!alarmInitialized(this) { 
    scheduleAlarms(this); 
}

}

public static void scheduleAlarms(Context context) {

    Calendar calendar = Calendar.getInstance();

    if(hasRunnedToday(context)) {       //if the alarm has run this day
        calendar.add(Calendar.DATE, 1); //schedule it to run again starting tomorrow
    }

    long firstRunTime = calendar.getTimeInMillis();

    AlarmManager mgr = (AlarmManager) context
            .getSystemService(Context.ALARM_SERVICE);
    Intent notificationIntent = new Intent(context, ReminderAlarm.class);
    PendingIntent pi = PendingIntent.getActivity(context, 0,
            notificationIntent, 0);

    mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP,
            firstRunTime, AlarmManager.INTERVAL_DAY, pi);
}

public static boolean alarmInitialized(Context context) {
    SharedPreferences preferences = context.getSharedPreferences("alarm_prefs", MODE_PRIVATE);

    long alarmLastRun = preferences.getLong("AlarmLastRun", -1);


    return alarmLastRun != -1;

}

public static void updateAlarmLastRun(Context context) {
    SharedPreferences preferences = context.getSharedPreferences("alarm_prefs", MODE_PRIVATE);


    preferences.edit()
            .putLong("AlarmLastRun", new Date().getTime())
        .apply();
}

public static boolean hasRunnedToday(Context context) {
    SharedPreferences preferences = context.getSharedPreferences("alarm_prefs", MODE_PRIVATE);

    long alarmLastRun = preferences.getLong("AlarmLastRun", -1);

    if(alarmLastRun == -1) {
        return false;
    }

    //check by comparing day, month and year
    Date now = new Date();
    Date lastRun = new Date(alarmLastRun);


    return now.getTime() - lastRun.getTime() < TimeUnit.DAYS.toMillis(1);
}

Each time your Reminder class alarm runs you should call updateAlarmLastRun to update the last time the alarm has run, this is necessary because the alarm may be schedule to be run on a day and the user reboots the device before the alarm has run in that case we don't want to use calendar.add(Calendar.DATE, 1); since that would skip a day.

On your Manifest.xml

<receiver android:name=".BootReceiver" android:enabled="true" android:exported="false" android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
     <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
    </receiver>

Notes:

  1. You shouldn't do context = this if context is a class field since the object holds a reference to its field context and context field holds a reference to the object that would leak
  2. Your Receiver 'onReceive` doesn't has the extras you assumed to have like "notificationCount" onReceive by the system when your device finish boot.
  3. Once your alarm runs call updateAlarmLastRun

Hope any of this helps