AlarmManager is sending in an inconsistent way pending intents with old data

I am trying to develop a simple reminder application and when the user creates a reminder I set an alarm through the ´AlarmManager´ class. For setting the alarm, I put the identifier of the reminder in the database inside of an intent, and then put that intent inside of a pending intent. When the alarm is received, the identifier of the reminder is taken from the received intent and searched in the database.

This approach is not working at all for me. Usually it behaves good, but sometimes the identifier that comes inside of the received intent is an old one and all the application fails. This happens randomly and I can't find a consistent way for reproducing it. As it is a random failure I am totally lost with this. It is totally absurd because the identifier that I receive in the extras of the intent is not the one which which I set the alarm. Instead, it is the identifier of an old alarm that was fired in the past.

package bembibre.alarmfix.alarms;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import java.util.Calendar;

import bembibre.alarmfix.database.RemindersDbAdapter;
import bembibre.alarmfix.logging.Logger;
import bembibre.alarmfix.utils.GeneralUtils;

/**
 * Created by Max Power on 12/08/2017.
 */

/**
 * Sets alarms in the operating system for the reminders of this application.
 */
public class ReminderManager {

    /**
     * This is the key that identifies a metadata item that is attached to the intent of an alarm of
     * a reminder for tracking it.
     */
    public static final String EXTRA_ALARM_ID = "extra_alarm_id";

    private Context mContext;
    private AlarmManager mAlarmManager;

    public ReminderManager(Context context) {
        mContext = context;
        mAlarmManager =
            (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    }

    /**
     * Part of the code that is responsible for setting an alarm.
     *
     * @param taskId  data base identifier of the reminder.
     * @param alarmId number that helps distinguishing each one of the alarms set for a same reminder.
     * @param when    when.
     */
    public void setReminder(long taskId, long alarmId, Calendar when) throws AlarmException {
        PendingIntent pi = getReminderPendingIntent(taskId, alarmId, PendingIntent.FLAG_UPDATE_CURRENT);

        try {
            this.setAlarm(pi, when);
            Logger.log("An alarm has been set successfully for the reminder at " + GeneralUtils.format(when) + ". Reminder id: " + taskId);
        } catch (Throwable throwable) {
            Logger.log("The system doesn't let us to set an alarm for the reminder at " + GeneralUtils.format(when), throwable);
            throw new AlarmException();
        }
    }

    /**
     * Unsets the alarm that would trigger for the reminder with the given database identifier.
     * When calling this method, the reminder could have been erased from the database and it
     * wouldn't be a problem. This method is only for unsetting its associated alarm from the
     * system.
     *
     * @param taskId  database identifier of the reminder.
     * @param alarmId number that helps distinguishing each one of the alarms set for a same reminder.
     * @param date    date for logging purposes.
     */
    public void unsetReminder(long taskId, long alarmId, String date) {
        PendingIntent pi = getReminderPendingIntent(taskId, alarmId, PendingIntent.FLAG_UPDATE_CURRENT);
        mAlarmManager.cancel(pi);
        Logger.log("An alarm has been unset successfully for the reminder at " + date + ". Reminder id: " + taskId);
    }

    /**
     * Returns the <code>PendingIntent</code> object that must be used for calling this application
     * when a reminder's alarm triggers.
     *
     * @param taskId  the number that identifies the associated reminder in the database.
     * @param alarmId incremental identifier for each alarm of the same reminder.
     * @param flag    flag that controls the behaviour of the pending intent.
     * @return the <code>PendingIntent</code> object.
     */
    private PendingIntent getReminderPendingIntent(long taskId, long alarmId, int flag) {
        Intent i = new Intent(mContext, OnAlarmReceiver.class);
        i.putExtra(RemindersDbAdapter.KEY_ROWID, taskId);
        i.putExtra(ReminderManager.EXTRA_ALARM_ID, alarmId);
        PendingIntent pi = PendingIntent.getBroadcast(mContext, (int)taskId, i, flag);
        return pi;
    }

    /**
     * Sets the alarm in the operating system.
     *
     * @param operation
     * @param when
     */
    private void setAlarm(PendingIntent operation, Calendar when) throws Throwable {
        /*
         * The alarm must be set differently depending on the OS version. Anyway, we need the
         * pending intent in order to know what was the reminder for which the alarm was fired, so
         * then the correct notification will be shown.
         */
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            // Before Marshmallow, we can do this for setting a reliable alarm.
            mAlarmManager.set(AlarmManager.RTC_WAKEUP, when.getTimeInMillis(), operation);
        } else {
            /*
             * Starting from Marshmallow, it seems like this is the only way for setting a reliable
             * alarm.
             * If we use the "alarm clock" framework, the user will see a icon of an alarm clock.
             * If we use the setExactAndAllowWhileIdle the user will see nothing, but the OS can
             * delay alarms at some sort of situations.
             */
            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, when.getTimeInMillis(), operation);
        }
    }
}

EDIT: The identifier comes correct when the alarm fires, but then, when I create another pending intent for a notification for opening the application with the desired identifier, it is there when the problem happens, in the pending intent that is used when I touch the notification. Here is the code that does this:

private void makeNotification(Long rowId, String title, String body) {
    android.app.NotificationManager mgr = (android.app.NotificationManager)context.getSystemService(NOTIFICATION_SERVICE);
    Intent notificationIntent;

    long notificationId;
    if (rowId == null) {
        notificationId = 0;
        notificationIntent = new Intent(context, ReminderListActivity.class);
    } else {
        notificationId = rowId;
        notificationIntent = new Intent(context, ReminderEditActivity.class);
        notificationIntent.putExtra(RemindersDbAdapter.KEY_ROWID, rowId);
    }
    PendingIntent pi = PendingIntent.getActivity(context, 0, notificationIntent,
            PendingIntent.FLAG_ONE_SHOT);

    NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
            .setSmallIcon(android.R.drawable.stat_sys_warning)
            .setContentTitle(title)
            .setContentText(body)
            .setContentIntent(pi)
            .setStyle(new NotificationCompat.BigTextStyle().bigText(body));;

    Notification note = mBuilder.build();

    note.defaults |= Notification.DEFAULT_SOUND;
    note.flags |= Notification.FLAG_AUTO_CANCEL;

    // An issue could occur if user ever enters over 2,147,483,647 tasks. (Max int value).
    // I highly doubt this will ever happen. But is good to note.
    int id = (int)((long)notificationId);
    mgr.notify(id, note);
    NotificationManager.setNotified(context, id);
}

EDIT: I found at least one case in which I can always reproduce the failure. Steps:

Upvotes: 3

Views: 409

Answers (2)

David Wasser
David Wasser

Reputation: 95628

Your answer is OK. As an alternative you could also change your code to use FLAG_UPDATE_CURRENT instead of FLAG_ONE_SHOT:

PendingIntent pi = PendingIntent.getActivity(context, 0, notificationIntent,
        PendingIntent.FLAG_UPDATE_CURRENT);

This will overwrite the "extras" in the existing PendingIntent.

Upvotes: 2

Regarding the steps for reproducing the failure that I posted above, it seems like when I discard the notification without going into it, its pending intent is still alive, so when I create another reminder the system uses the old pending intent with the old intent inside of it, which in turn has got the old identifier.

The problem seems to be resolved by passing a different number in the second parameter of the method PendingIntent.getActivity() (requestCode), so all pending intents are different, and the system always creates a new one instead of trying to use the old one. As the request code the best approach is to use the database identifier of the reminder, thus, each reminder gets notified with its own pending intent.

Upvotes: 0

Related Questions