DP5 7.0 - 向未决意图添加额外内容会失败吗?

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

在跟踪器上添加链接问题: https://code.google.com/p/android/issues/detail?id=216581&thanks=216581&ts=1468962325

所以我今天在我的 Nexus 5X 上安装了 DP5 Android 7.0 版本。我一直在开发一个应用程序,它使用 Android 的 AlarmManager class 在特定时间安排本地通知。在此版本之前,代码在设备 运行 KitKat、Lollipop 和 Marshmallow 上运行良好。

以下是我安排闹钟的方式:

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);
                }

我的 "SET_NOTIFICATION_INTENT" 的 AlarmManagerUtil @onReceive 看起来像这样:

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

奇怪的是 "reminderToFire" 在这里 仅在 Android N 设备上 但是 timeToFire 是正确的。

我认为它与 Parceler 库有关?我正在使用 Java 1.8 和目标 Android API 24 进行编译。

我确实在网上四处寻找答案,但我的情况有点独特,因为代码 100% 适用于 Android 的所有先前版本(N 预览版以下的所有内容)。 .so 我尽可能地遵循以下答案:

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

还有其他人有这个问题吗?

我之前曾看到过此类行为的报告,涉及自定义 Parcelable 对象和系统服务(例如,NotificationManager)。似乎发生的是系统尝试使用 PendingIntent,并且出于某种原因它试图取消 Parcel Parcelable 作为其中的一部分。这失败了,因为系统没有你的 类。我已经有一段时间没听说有人 运行 参与其中了,但完全有可能 Android N 中出现回归并重新引入它。

您可能会翻阅 LogCat 以查看是否有任何来自系统(而不是您的应用程序)的消息——或者更好的是堆栈跟踪——似乎与您的警报事件有关。

如果您可以创建可重现的测试用例,请在 the Android issue tracker 上提交问题。如果你想到它,post 一个 link 到这里,因为我想看看它。

关于解决方法,我可以想到两个:

  1. 不要把 Parcelable 放在那里。相反,放置一个 ID,您可以根据需要使用该 ID 来查找信息,无论是从内存中的缓存(如果您的进程恰好还在)还是从您的持久数据存储中。

  2. Parcelable 切换到我和其他人所说的 "bundle-able",您可以在其中将对象与 Bundle 相互转换。基本上,仅坚持 OS 定义的 类,没有自定义 类。然后,系统可以安全地取消 ParcelBundle(无论出于何种原因)。当然,这比简单地使用注解处理器来创建 Parcelable 实现要痛苦得多。

对于任何最终在这里通过 AlarmManager 拉扯头发的人(并且还没有放弃并转到 JobScheduler),Google 在生产 API 24 构建中不支持传递将 Parcelable 对象放入 AlarmManager。

我解决这个问题的方式: 如果您需要将列表(或单个对象)发送到 AlarmManager,请将该项目作为字符串存储到 SharedPreferences 中。 (Gson.toJson(object, type)) 如果对象是接口,则有许多接口适配器解决方案。我发现一个漂浮在 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;
}
}

设置适配器后,如果您正在使用某种 DI 框架(即 Dagger2),则无需每次都使用 TypeAdapter 设置 GS0N...

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

所以你所要做的就是运行.....

/**
 * 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();
}

希望这对您有所帮助。当然,当您需要从共享首选项中获取此对象时....

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

执行典型的 List 对象 = gson.fromJson(json, type) 应该可行。

干杯。

我发现将 Parcelable 包装在 Bundle 中是可行的。

// 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);

但是,我不确定这种方法是否适合未来。我已经在 android 错误跟踪器上评论了这个问题:https://code.google.com/p/android/issues/detail?id=209422#c11 但我怀疑它是否会收到回复,因为这个问题已经被标记为已关闭。