android - PendingIntent.getActivity() 处的 NullPointerException
问题描述
我正在开发一个具有提醒功能的应用程序。提醒保存在 sqlite 中,当设备重启提醒再次使用 BootReceiver 创建时。
NullPointerException 在某些设备中发生。
根据 crashlytics 报告,应用程序处于后台 100%
这是 crashlytics 日志:
Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.Intent.migrateExtraStreamToClipData()' on a null object reference
at android.app.PendingIntent.getActivity(PendingIntent.java:345)
at android.app.PendingIntent.getActivity(PendingIntent.java:308)
at test.reminder.services.AlarmReceiver.launchAlarmLandingPage(AlarmReceiver.java:231)
at test.reminder.services.AlarmReceiver.access$000(AlarmReceiver.java:33)
at test.reminder.services.AlarmReceiver$ScheduleAlarm.schedule(AlarmReceiver.java:279)
at test.reminder.services.AlarmReceiver.setReminderAlarm(AlarmReceiver.java:122)
at test.reminder.services.AlarmReceiver.setReminderAlarms(AlarmReceiver.java:127)
at test.reminder.services.BootReceiver.lambda$onReceive$0(BootReceiver.java:23)
at test.reminder.services.-$$Lambda$BootReceiver$MU2rWs8I8r27tAltl1VUIV_8WwI.run(-.java:2)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
以下是相关代码:
public final class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = AlarmReceiver.class.getSimpleName();
private static final String CHANNEL_ID = "alarm_channel";
private static final String BUNDLE_EXTRA = "bundle_extra";
private static final String ALARM_KEY = "alarm_key";
@Override
public void onReceive(Context context, Intent intent) {
final Alarm alarm = intent.getBundleExtra(BUNDLE_EXTRA).getParcelable(ALARM_KEY);
if (alarm == null) {
Log.e(TAG, "Alarm is null", new NullPointerException());
return;
}
final int id = alarm.notificationId();
final NotificationManager manager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
createNotificationChannel(context);
String content = "";
String title = "";
if (alarm.getLabel().equals(ReminderLabelSelectionBottomSheetDialog.LABEL_BREATH_EXERCISE)) {
content = context.getString(R.string.breath_exercise_reminder_content_message);
title = context.getString(R.string.hello);
} else if (alarm.getLabel().equals(ReminderLabelSelectionBottomSheetDialog.LABEL_AFFIRMATION)) {
content = context.getString(R.string.affirmation_reminder_content_message);
title = context.getString(R.string.hello);
} else if (alarm.getLabel().equals(ReminderLabelSelectionBottomSheetDialog.LABEL_MEDITATION)) {
content = context.getString(R.string.meditation_reminder_content_message);
title = context.getString(R.string.hello);
} else if (alarm.getLabel().equals(ReminderLabelSelectionBottomSheetDialog.LABEL_MINDFUL_PRACTICES)) {
content = context.getString(R.string.practice_reminder_content_message);
title = context.getString(R.string.hello);
} else if (alarm.getLabel().equals(ReminderLabelSelectionBottomSheetDialog.LABEL_SLEEP)) {
content = context.getString(R.string.sleep_reminder_content_message);
title = context.getString(R.string.sleep_time);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
builder.setSmallIcon(R.drawable.ic_logo_notification);
builder.setColor(ContextCompat.getColor(context, R.color.white));
builder.setContentTitle(title);
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(content));
builder.setContentText(content);
builder.setTicker(content);
builder.setDefaults(Notification.DEFAULT_SOUND);
builder.setDefaults(Notification.DEFAULT_LIGHTS);
builder.setContentIntent(launchAlarmLandingPage(context, alarm));
builder.setAutoCancel(true);
builder.setPriority(Notification.PRIORITY_HIGH);
if (manager != null && alarm.isEnabled()) {
manager.notify(id, builder.build());
}
//Reset Alarm manually
setReminderAlarm(context, alarm);
}
//Convenience method for setting a notification
public static void setReminderAlarm(Context context, Alarm alarm) {
Log.d("Alarming", "set reminder");
if (!alarm.isEnabled()) {
Log.d("Alarming", "set reminder - alarm is not enabled");
cancelReminderAlarm(context, alarm);
return;
}
final Calendar nextAlarmTime = getTimeForNextAlarm(alarm);
alarm.setTime(nextAlarmTime.getTimeInMillis());
final Intent intent = new Intent(context, AlarmReceiver.class);
final Bundle bundle = new Bundle();
bundle.putParcelable(ALARM_KEY, alarm);
intent.putExtra(BUNDLE_EXTRA, bundle);
final PendingIntent pIntent = PendingIntent.getBroadcast(
context,
alarm.notificationId(),
intent,
FLAG_UPDATE_CURRENT
);
ScheduleAlarm.with(context).schedule(alarm, pIntent);
}
public static void setReminderAlarms(Context context, List<Alarm> alarms) {
for (Alarm alarm : alarms) {
setReminderAlarm(context, alarm);
}
}
/**
* Calculates the actual time of the next alarm/notification based on the user-set time the
* alarm should sound each day, the days the alarm is set to run, and the current time.
*
* @param alarm Alarm containing the daily time the alarm is set to run and days the alarm
* should run
* @return A Calendar with the actual time of the next alarm.
*/
private static Calendar getTimeForNextAlarm(Alarm alarm) {
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(alarm.getTime());
final long currentTime = System.currentTimeMillis();
final int startIndex = getStartIndexFromTime(calendar);
int count = 0;
boolean isAlarmSetForDay;
final SparseBooleanArray daysArray = alarm.getDays();
do {
final int index = (startIndex + count) % 7;
isAlarmSetForDay =
daysArray.valueAt(index) && (calendar.getTimeInMillis() > currentTime);
if (!isAlarmSetForDay) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
count++;
}
} while (!isAlarmSetForDay && count < 7);
return calendar;
}
public static void cancelReminderAlarm(Context context, Alarm alarm) {
Log.d("Alarming", "cancel reminder alarm");
final Intent intent = new Intent(context, AlarmReceiver.class);
final PendingIntent pIntent = PendingIntent.getBroadcast(
context,
alarm.notificationId(),
intent,
FLAG_UPDATE_CURRENT
);
final AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (manager != null) {
manager.cancel(pIntent);
}
}
private static int getStartIndexFromTime(Calendar c) {
final int dayOfWeek = c.get(Calendar.DAY_OF_WEEK);
int startIndex = 0;
switch (dayOfWeek) {
case Calendar.MONDAY:
startIndex = 0;
break;
case Calendar.TUESDAY:
startIndex = 1;
break;
case Calendar.WEDNESDAY:
startIndex = 2;
break;
case Calendar.THURSDAY:
startIndex = 3;
break;
case Calendar.FRIDAY:
startIndex = 4;
break;
case Calendar.SATURDAY:
startIndex = 5;
break;
case Calendar.SUNDAY:
startIndex = 6;
break;
}
return startIndex;
}
private static void createNotificationChannel(Context ctx) {
if (SDK_INT < O) return;
final NotificationManager mgr = ctx.getSystemService(NotificationManager.class);
if (mgr == null) return;
final String name = ctx.getString(R.string.channel_name);
if (mgr.getNotificationChannel(name) == null) {
final NotificationChannel channel =
new NotificationChannel(CHANNEL_ID, name, IMPORTANCE_HIGH);
mgr.createNotificationChannel(channel);
}
}
private static PendingIntent launchAlarmLandingPage(Context ctx, Alarm alarm) {
return PendingIntent.getActivity(
ctx, alarm.notificationId(), launchIntent(ctx, alarm.getLabel()), FLAG_UPDATE_CURRENT
);
}
public static Intent launchIntent(Context context, String type) {
//Normalde labela göre farklı activityler başlatmamız gerek
// ancak biz çoğu sayfa için fragment kullandığımız için yalnızca MainActivitye yönlendiriyoruz
final Intent i = new Intent(context, SplashScreenActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return i;
}
private static class ScheduleAlarm {
@NonNull
private final Context ctx;
@NonNull
private final AlarmManager am;
private ScheduleAlarm(@NonNull AlarmManager am, @NonNull Context ctx) {
this.am = am;
this.ctx = ctx;
}
static ScheduleAlarm with(Context context) {
final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (am == null) {
throw new IllegalStateException("AlarmManager is null");
}
return new ScheduleAlarm(am, context);
}
void schedule(Alarm alarm, PendingIntent pi) {
if (SDK_INT > LOLLIPOP) {
am.setAlarmClock(new AlarmManager.AlarmClockInfo(alarm.getTime(), launchAlarmLandingPage(ctx, alarm)), pi);
} else {
am.setExact(AlarmManager.RTC_WAKEUP, alarm.getTime(), pi);
}
}
}
}
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Executors.newSingleThreadExecutor().execute(() -> {
final List<Alarm> alarms = DatabaseHelper.getInstance(context).getAlarms();
setReminderAlarms(context, alarms);
});
}
}
}
这是我使用的 Alarm 类:
public final class Alarm implements Parcelable {
private Alarm(Parcel in) {
id = in.readLong();
time = in.readLong();
label = in.readString();
allDays = in.readSparseBooleanArray();
isEnabled = in.readByte() != 0;
}
public static final Creator<Alarm> CREATOR = new Creator<Alarm>() {
@Override
public Alarm createFromParcel(Parcel in) {
return new Alarm(in);
}
@Override
public Alarm[] newArray(int size) {
return new Alarm[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(id);
parcel.writeLong(time);
parcel.writeString(label);
parcel.writeSparseBooleanArray(allDays);
parcel.writeByte((byte) (isEnabled ? 1 : 0));
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({MON, TUES, WED, THURS, FRI, SAT, SUN})
@interface Days {
}
public static final int MON = 1;
public static final int TUES = 2;
public static final int WED = 3;
public static final int THURS = 4;
public static final int FRI = 5;
public static final int SAT = 6;
public static final int SUN = 7;
private static final long NO_ID = -1;
private final long id;
private long time;
private String label;
private SparseBooleanArray allDays;
private boolean isEnabled;
public Alarm() {
this(NO_ID);
}
public Alarm(long id) {
this(id, System.currentTimeMillis());
}
public Alarm(long id, long time, @Days int... days) {
this(id, time, null, days);
}
public Alarm(long id, long time, String label, @Days int... days) {
this.id = id;
this.time = time;
this.label = label;
this.allDays = buildDaysArray(days);
}
public long getId() {
return id;
}
public void setTime(long time) {
this.time = time;
}
public long getTime() {
return time;
}
public void setLabel(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
public void setDay(@Days int day, boolean isAlarmed) {
allDays.append(day, isAlarmed);
}
public SparseBooleanArray getDays() {
return allDays;
}
public boolean getDay(@Days int day) {
return allDays.get(day);
}
public void setIsEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public boolean isEnabled() {
return isEnabled;
}
public int notificationId() {
final long id = getId();
return (int) (id ^ (id >>> 32));
}
@Override
public String toString() {
return "Alarm{" +
"id=" + id +
", time=" + time +
", label='" + label + '\'' +
", allDays=" + allDays +
", isEnabled=" + isEnabled +
'}';
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (int) (id ^ (id >>> 32));
result = 31 * result + (int) (time ^ (time >>> 32));
result = 31 * result + label.hashCode();
for (int i = 0; i < allDays.size(); i++) {
result = 31 * result + (allDays.valueAt(i) ? 1 : 0);
}
return result;
}
private static SparseBooleanArray buildDaysArray(@Days int... days) {
final SparseBooleanArray array = buildBaseDaysArray();
for (@Days int day : days) {
array.append(day, true);
}
return array;
}
private static SparseBooleanArray buildBaseDaysArray() {
final int numDays = 7;
final SparseBooleanArray array = new SparseBooleanArray(numDays);
array.put(MON, false);
array.put(TUES, false);
array.put(WED, false);
array.put(THURS, false);
array.put(FRI, false);
array.put(SAT, false);
array.put(SUN, false);
return array;
}
}
解决方案
问题是AlarmReceiver.java:231
:
该文档还解释了它:PendingIntent.getActivity()
标志Intent.FLAG_ACTIVITY_NEW_TASK
需要添加到Intent
:
private static PendingIntent launchAlarmLandingPage(Context context, Alarm alarm) {
Intent intent = new Intent(context, SplashScreenActivity.class);
// intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return PendingIntent.getActivity(
context,
alarm.notificationId(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT
);
}