Search code examples
androidalarmmanagerandroid-notificationsparcelerandroid-7.0-nougat

DP5 7.0 - Does adding extras to a pending intent fail?


Adding the linked issue on tracker: https://code.google.com/p/android/issues/detail?id=216581&thanks=216581&ts=1468962325

So I installed the DP5 Android 7.0 release onto my Nexus 5X today. I've been working on an app that schedules local notifications at specific times using Android's AlarmManager class. Up until this release, the code has been working great on devices running KitKat, Lollipop, and Marshmallow.

Below is how I'm scheduling the alarms:

Intent intent = new Intent(context, AlarmManagerUtil.class);
            intent.setAction(AlarmManagerUtil.SET_NOTIFICATION_INTENT);
            intent.putExtra(AlarmManagerUtil.REMINDER_EXTRA, Parcels.wrap(reminders));
            intent.putExtra("time", when.getMillis());
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            if (alarmManager != null) {
                if (Build.VERSION.SDK_INT >= 23) {
                  alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, when.getMillis(), pendingIntent);
                } else if (Build.VERSION.SDK_INT >= 19) {
                    alarmManager.setExact(AlarmManager.RTC_WAKEUP, when.getMillis(), pendingIntent);
                } else {
                    alarmManager.set(AlarmManager.RTC_WAKEUP, when.getMillis(), pendingIntent);
                }

My AlarmManagerUtil @onReceive of the "SET_NOTIFICATION_INTENT" looks like this:

public void fireNotification(Context context, Intent intent) {
    List<Reminder> reminderToFire = Parcels.unwrap(intent.getParcelableExtra(REMINDER_EXTRA));
    long timeToFire = intent.getLongExtra("time", 0L); //.... }

What's strange is the "reminderToFire" is null here only on Android N devices but the timeToFire is correct.

I'm thinking its something to do with the Parceler Library? I'm compiling using Java 1.8 and targeting Android API 24.

I've definitely looked around the net for an answer to this, but my case is a bit unique since the code 100% works on all prior versions of Android (everything below N preview)...so I am following the below answers as much as I can:

How can I correctly pass unique extras to a pending intent?

Anybody else have this issue?


Solution

  • For anyone ending up here pulling your hair out over AlarmManager (and haven't given up and gone to JobScheduler yet), Google in the production API 24 build does not support passing a Parcelable object into the AlarmManager.

    The way I got around this: If you need to send a List (or single object) into the AlarmManager, store the item into SharedPreferences as a String. (Gson.toJson(object, type)) If the object is an interface, there are a number of interface adapter solutions out there. One I found floating around S/O:

    public final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
    
    public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
        final JsonObject wrapper = new JsonObject();
        wrapper.addProperty("type", object.getClass().getName());
        wrapper.add("data", context.serialize(object));
        return wrapper;
    }
    
    public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException {
        final JsonObject wrapper = (JsonObject) elem;
        final JsonElement typeName = get(wrapper, "type");
        final JsonElement data = get(wrapper, "data");
        final Type actualType = typeForName(typeName);
        return context.deserialize(data, actualType);
    }
    
    private Type typeForName(final JsonElement typeElem) {
        try {
            return Class.forName(typeElem.getAsString());
        } catch (ClassNotFoundException e) {
            throw new JsonParseException(e);
        }
    }
    
    private JsonElement get(final JsonObject wrapper, String memberName) {
        final JsonElement elem = wrapper.get(memberName);
        if (elem == null)
            throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper");
        return elem;
    }
    }
    

    Once you have the adapter set up, you won't need to set up GS0N each time with the TypeAdapter if you're using some sort of DI framework (i.e. Dagger2) like so...

    @Singleton
    @Provides
    public Gson providesGson() {
        return new GsonBuilder()
                .registerTypeAdapter(YourInterfaceClass.class, new InterfaceAdapter<YourInterfaceClass>())
                .create();
    

    So all you'll have to do is run....

    /**
     * stores yourInterfaceClass in shared prefs
     */
    public void setNextReminder(List<YourInterfaceClass> yourInterfaceClass) {
        Type type = new TypeToken<List<YourInterfaceClass>>() {}.getType();
        sharedPrefs.edit().putString(YOUR_KEY, gson.toJson(yourInterfaceClass, type)).apply();
    }
    

    Hope this helps. Of course, when you need to get this object out of shared prefs....

    String json = sharedPrefs.getString(YOUR_KEY, "No object found");
    

    Doing the typical List object = gson.fromJson(json, type) should work.

    Cheers.