8

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?

Community
  • 1
  • 1
Aceofspadez44
  • 234
  • 2
  • 11
  • Have you looked to see if `Parcels.wrap(reminders)` is returning `null`? Have you looked to see if `intent.getParcelableExtra(REMINDER_EXTRA)` is returning `null`, before you pass that value to `Parcels.unwrap()`? Have you tried stuffing some other `Parcelable` into the `Intent` to see if it survives the trip (e.g., a `Point`)? – CommonsWare Jul 19 '16 at 18:51
  • @CommonsWare the `Intent` being passed into the `PendingIntent` argument above is being loaded with the `mExtras` object with `mMap` containing two objects, the `Long timeToFire` and the Parceled reminder object. Upon unwrap in the `AlarmManager onReceive`, the Long is valid but the `reminder` object is null. I'll try to Parcel some other object like a `Point` and see how it goes. `Reminder` is correctly configured with `@Parcel` – Aceofspadez44 Jul 19 '16 at 18:59
  • "Upon unwrap in the AlarmManager onReceive, the Long is valid but the reminder object is null" -- but, is `intent.getParcelableExtra(REMINDER_EXTRA)` `null`? That would indicate the value is getting lost. If `intent.getParcelableExtra(REMINDER_EXTRA)` is not `null`, but `Parcels.unwrap(intent.getParcelableExtra(REMINDER_EXTRA))` is `null`, that suggests that `Parcels` is having difficulty restoring the objects. – CommonsWare Jul 19 '16 at 19:08
  • @CommonsWare ah, no `intent.getParcelableExtra(REMINDER_EXTRA)` correctly has the data; however, the `mExtras` object within the intent is missing the `mMap` object until it attempts to get the extras. Debugging now, it actually looks like unwrap is getting called with a `Parcelable input = null` and therefore returns `null`. This is not the case with devices running prior versions of Android...I'll open an issue with the `Parceler` library on Github. Thank you sir. – Aceofspadez44 Jul 19 '16 at 19:15
  • "it actually looks like unwrap is getting called with a Parcelable input = null and therefore returns null" -- I am confused as to how this meshes with "intent.getParcelableExtra(REMINDER_EXTRA) correctly has the data". – CommonsWare Jul 19 '16 at 19:26
  • @CommonsWare Ah, I mean `intent.getParcelableExtra(REMINDER_EXTRA)` is null and thus `Parcels.unwrap(null)` returns `null`, but the `intent.getAction()` correctly displays and the `intent.getLong()` is correct as well. Sorry. You're correct. The issue seems to lie between the `Intent` getting set and the `BroadcastReceiver onReceive`, even though `Parcels.wrap(reminder)` successfully Parcels the object for the Intent. – Aceofspadez44 Jul 19 '16 at 19:35
  • I'm having the same issue but I get nulls returned from both getString and getParcelableArrayList. I didn't see your posting before I posted mine. - see http://stackoverflow.com/questions/38775285/android-7-broadcastreceiver-onreceive-intent-getextras-missing-data – Phil O Aug 05 '16 at 11:26

3 Answers3

8

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.

Aceofspadez44
  • 234
  • 2
  • 11
  • 1
    A probably-faster approach would be to convert the `Parcelable` to a `byte[]`, which somebody pointed out to me when [I blogged about this issue](https://commonsware.com/blog/2016/07/22/be-careful-where-you-use-custom-parcelables.html). See http://stackoverflow.com/a/18000094/115145 for a sample implementation. – CommonsWare Aug 31 '16 at 20:55
  • @CommonsWare, nice. That is certainly a way to go about it. The risk is still there when working with Parcelable, although my solution depends on GSON (or some other serializer) to implement. In any case either solution certainly works. – Aceofspadez44 Sep 01 '16 at 14:39
  • 2
    I don't think you need to pass via shared preferences. Actually base types are still working, you could pass the json string via the intent and parse it with Gson when receiving it. Also I noticed no troubles with passing a Bitmap... (which implements Parcelable) – ilansas Oct 06 '16 at 08:44
  • Yep, as ilansas mentioned, it is enough to send it as a String. – AlvaroSantisteban Nov 01 '16 at 14:16
  • Fortunately, I am always in a good practice of making JSON out of my POJOs – Neon Warge Jul 14 '17 at 03:14
2

I have seen this sort of behavior reported before, with custom Parcelable objects and system services (e.g., NotificationManager). What seems to happen is that the system tries using the PendingIntent, and as part of that for some reason it tries to un-Parcel the Parcelable. This fails, because the system doesn't have your classes. I haven't heard of somebody running into this in a while, but it's entirely possible that there is a regression in Android N that re-introduced it.

You might rummage through LogCat to see if there are any messages — or, better yet, stack traces — from the system (not your app) that seem to pertain to your alarm event.

If you can create a reproducible test case, file an issue on the Android issue tracker. If you think of it, post a link to it here, as I'd like to take a peek at it.

In terms of workarounds, I can think of two:

  1. Don't put the Parcelable in there. Instead, put an ID that you can use to look up the information as needed, whether from an in-memory cache (if your process happens to still be around) or from whatever your persistent data store is.

  2. Switch from Parcelable to what I and others have termed "bundle-able", where you convert your object to and from a Bundle. Basically, stick solely to OS-defined classes, with no custom classes. Then, the system can safely de-Parcel the Bundle (for whatever reason it does so). This, of course, is much more painful than simply using an annotation processor to create the Parcelable implementation.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    You're definitely correct, I will certainly post an issue up on the tracker. It has to be something with the N sources (which we cannot see yet since it is not officially released) – Aceofspadez44 Jul 19 '16 at 20:14
  • @Aceofspadez44: NDP5 sources are not available yet, but [NDP4 is](https://android.googlesource.com/platform/frameworks/base/+/android-n-preview-4). – CommonsWare Jul 19 '16 at 20:15
  • nice! I did compile this project against NDP4 as well and got the same results. Thanks again for the help. I'll have to bundle it OR stick the object in shared prefs with a key and handle it later. – Aceofspadez44 Jul 19 '16 at 20:39
  • https://code.google.com/p/android/issues/detail?id=216581&thanks=216581&ts=1468962325 – Aceofspadez44 Jul 19 '16 at 21:06
  • @Aceofspadez44: Without a reproducible test case, I do not hold a lot of hope for your issue. – CommonsWare Jul 19 '16 at 21:06
  • good point, I'll set something up using the AlarmManager sample from the googlesamples repo and fix the issue with something to reproduce. – Aceofspadez44 Jul 19 '16 at 21:11
  • https://github.com/CCorrado/android-RepeatingAlarm if you're interested in reproducing this issue yourself, I spun something up quickly that reproduces the issue. – Aceofspadez44 Jul 19 '16 at 22:04
  • @Aceofspadez44: I see that you asked about this [on the Reddit AMA](https://www.reddit.com/r/androiddev/comments/4tm8i6/were_on_the_android_engineering_team_and_built/d5ilrxo), and Dianne echoed the concern in my answer. I'm a bit surprised that there's a change in behavior, though -- I would have expected it to either always have been failing or to still work. I'll take a peek at your sample sometime soonish. Thanks! – CommonsWare Jul 19 '16 at 22:09
  • Right! I did indeed. I agree with you, why would this suddenly be a problem on N but not other OSes. Sounds good, let me know if you find a solution using Parcelable, I'll probably just cache the object in/out using SharedPreferences as a fix for this project. – Aceofspadez44 Jul 19 '16 at 23:13
  • @Aceofspadez44: Turns out this was reported as [an issue](https://code.google.com/p/android/issues/detail?id=209422) nearly two months ago. The Google engineers had the same reaction that Dianne did, and were as surprised as I was that this is a change in behavior. However, they deem this as working as intended. – CommonsWare Jul 22 '16 at 19:14
  • I got the notification today, took a look....looks like I can serialize a String into SharedPrefs with the parcelable, then parse the string out and create a Parcelable from that String. Might need some trickery with GSON but I think i can get it to work.. – Aceofspadez44 Jul 22 '16 at 19:55
2

I've found that wrapping the Parcelable in a Bundle works.

// When setting up the PendingIntent for the AlarmManager:
Intent intent = new Intent(context, MyService.class);
MyParcelable myParcelable = new MyParcelable();
Bundle b = new Bundle();
b.putParcelable(EXTRA_MY_PARCELABLE, myParcelable);
intent.putExtra(EXTRA_BUNDLE, b);
PendingIntent.getService(0, intent, 0);

// From the Service (or Activity, BroadcastReceiver, etc.):
Bundle b = intent.getExtra(EXTRA_BUNDLE);
MyParcelable myParcelable = b.getParcelableExtra(EXTRA_MY_PARCELABLE);

However, I'm not sure how future-proof this approach is. I've commented on the issue on the android bug tracker: https://code.google.com/p/android/issues/detail?id=209422#c11 but I doubt it's going to receive a response since the issue has already been marked closed.

Noel
  • 7,350
  • 1
  • 36
  • 26