#java #android #scheduled-tasks #scheduler #android-jetpack
#java #Android #запланированные задачи #планировщик #android-реактивный ранец
Вопрос:
В следующих выдержках кода я использую a BroadcastReceiver
для запуска a Service
при загрузке устройства и / или перезагрузке пакета. Это NotificationService
вызывает мой Worker
via PeriodicWorkRequest
каждые пятнадцать минут. Изначально все работает так, как должно, пока NotificationWorker
не будет выполнено. Кажется, что в момент Worker
вызова он выполняется двадцать раз, а не только один раз. Я считаю, что это ровно в двадцать раз. После того, как все сказано и сделано, он ожидает ~ 15 минут, как и должно быть, а затем демонстрирует то же поведение, когда он снова вызывает Worker
. В идеале это Worker
должно выполняться только один раз каждые 15 м, особенно потому, что некоторые вычисления, которые он будет выполнять, довольно дороги.
Я потратил несколько дней на поиск в Google дополнительной информации (мне частично мешало это из-за использования Work v1.0.1 вместо более поздней версии androidx v2.4.0, но я не готов обновлять все, что было бы нарушено в моем проекте с этим изменением)и делаю все возможное, чтобы отладить эту проблему. К сожалению, отладка была довольно медленной и непродуктивной из-за того, что я редко могу заставить свои Log.?()
сообщения даже отображаться, не говоря уже о том, чтобы дать мне какие-либо подсказки относительно того, откуда исходит такое поведение. Такое поведение ( Log
сообщения не отображаются) было проблемой в BootServiceStart
, NotificationWorker
, и NotificationService
, и я понятия не имею, почему.
Вот применимый код для решения проблемы; пожалуйста, обратите внимание, что если вы перейдете по ссылкам dpaste, вы обнаружите, что общие области проблемного кода выделены, чтобы немного облегчить диагностику (dpasted код будет доступен только еще 6 дней):
BootServiceStart
— также здесь, на dpaste
package com.example.sprite.half_lifetimer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
public class BootServiceStart extends BroadcastReceiver {
public void onReceive(Context context, Intent arg1) {
Intent intent = new Intent(context , NotificationService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
if (GlobalMisc.Debugging) {
Log.i("Halflife.BootServiceStart", "Attempted to start NotificationService");
}
}
}
NotificationService
— также здесь, на dpaste
package com.example.sprite.half_lifetimer;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
public class NotificationService extends Service {
public static HashMap<Integer, Boolean> firedNotifications = new HashMap<>();
public static LocalDateTime lastNotificationLoopLDT = null;
@Nullable
public IBinder onBind(Intent intent) {
return null;
}
/**
* Method handles creation of a NotificationChannel and database
* initialization (for this particular subset of the code), then passing
* control off to notificationLoop().
*/
public void onCreate() {
startForeground(31337, buildForegroundNotification());
if (GlobalMisc.Debugging) {
Log.i("Halflife.NotificationService.onCreate", "Started NotificationService");
}
// Create the NotificationChannel, but only on API 26 because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(
"taper-n-clearing-talk", "taper-n-clearing",
NotificationManager.IMPORTANCE_NONE);
chan.setDescription("Notifications for Taper dosages and Substance clearance");
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(chan);
}
//get the database ready
try {
Permanence.Misc.init(/*NotificationService.this*/ getApplicationContext());
} catch (Exception ex) {
Log.e("Halflife.notificationLoop", "Unable to init database: "
ex.toString());
}
if (GlobalMisc.Debugging) {
Log.i("Halflife.onCreate", "all valid tapers: "
Permanence.Tapers.loadAllValidTapers(getApplicationContext()).toString());
}
//notificationLoop();
PeriodicWorkRequest notificationsRequest =
new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance()
.enqueue(notificationsRequest);
}
private Notification buildForegroundNotification() {
NotificationCompat.Builder b=new NotificationCompat.Builder(this);
b.setOngoing(true)
.setContentTitle("HLT Foreground Service")
.setContentText("Giving Half-life Timer foreground priority")
.setChannelId("taper-n-clearing-talk")
.setSmallIcon(getApplicationContext().getResources().getIdentifier(
"plus_medical_blue","drawable",
getApplicationContext().getPackageName()));
return(b.build());
}
}
NotificationWorker
— также здесь, на dpaste
package com.example.sprite.half_lifetimer;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class NotificationWorker extends Worker {
private boolean notificationDebugging = false;
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@Override
public Result doWork() {
LocalDateTime nextScheduledDosage;
long adminDurationMinutes;
if (!notificationDebugging) {
if (GlobalMisc.NotificationsEnabled) {
//taper notification loop
for (Taper taper : Permanence.Tapers.loadAllValidTapers(getApplicationContext())) {
//this will handle if any tapers have been added since inception
if (!NotificationService.firedNotifications.containsKey(taper.getId())) {
NotificationService.firedNotifications.put(taper.getId(), false);
}
//if this is a constrained taper, but we're outside of the window, just
//go on to the next taper
if (taper.isConstrained() amp;amp; !taper.inConstraintHours()) {
Log.i("Halflife.notificationLoop",
"skipping " taper.toString()
" (outside of hourly constraints)");
continue;
}
if (!NotificationService.firedNotifications.get(taper.getId())) {
try {
nextScheduledDosage = taper.findNextScheduledDosageLDT();
if (!taper.isConstrained()) {
Log.i("Halflife.notificationLoop",
"working with unconstrained taper");
adminDurationMinutes = Duration.ofDays(1).dividedBy(
taper.getAdminsPerDay()).toMinutes();
} else {
Log.i("Halflife.notificationLoop",
"working with constrained taper");
//not sure if this is necessary or not, but might as well
//throw it in since the goddamned code is too complex for me
//to follow right now down below
LocalTime nextDosageTime =
nextScheduledDosage.toLocalTime();
if (nextDosageTime.isBefore(taper.getStartHour()) ||
nextDosageTime.isAfter(taper.getEndHour())) {
Log.i("notificationLoop",
"skipping " taper.toString()
" (outside of constraint hours)");
continue;
}
//this part, of course, is necessary
adminDurationMinutes =
Duration.between(taper.getStartHour(),
taper.getEndHour()).dividedBy(
taper.getAdminsPerDay())
.toMinutes();
}
if (GlobalMisc.Debugging) {
Log.i("Halflife.notificationLoop", "Checking taper: "
taper.getName());
Log.i("Halflife.notificationLoop", "nextScheduledDosage "
"contains: " nextScheduledDosage.toString());
}
if (((NotificationService.lastNotificationLoopLDT != null) amp;amp;
nextScheduledDosage.isAfter(
NotificationService.lastNotificationLoopLDT) amp;amp;
nextScheduledDosage.isBefore(
LocalDateTime.now().plusMinutes(
(adminDurationMinutes / 5)))) ||
(nextScheduledDosage.isAfter(
LocalDateTime.now().minusMinutes(1)) amp;amp;
nextScheduledDosage.isBefore(
LocalDateTime.now().plusMinutes(
(adminDurationMinutes / 5))))) {
fireTaperNotification(taper);
//set firedNotifications to reflect that we sent this
//notification
NotificationService.firedNotifications.replace(taper.getId(), true);
} else if (GlobalMisc.Debugging) {
Log.i("Halflife.notificationLoop",
"not displaying notification as per "
"datetime constraints");
}
} catch (Exception ex) {
Log.e("Halflife.notificationLoop",
"Issue finding next scheduled dosage: "
ex.toString());
return Result.failure();
}
}
}
} else {
GlobalMisc.debugMsg("NotificationWorker:doWork",
"Would have just gone into substance taper notification loop");
}
if (GlobalMisc.NotificationsEnabled) {
//substance clearing notification loop
//LocalDateTime fiveMinAgo = LocalDateTime.now().minusMinutes(5);
for (Substance sub : Permanence.Subs.loadUnarchivedSubstances(
getApplicationContext())) {
if (GlobalMisc.Debugging) {
Log.i("Halflife.notificationLoop",
"Checking sub clearance: " sub.getCommon_name());
}
//has this substance cleared within the last 5 minutes?
LocalDateTime clearedAt = sub.getFullEliminationLDT();
if (clearedAt != null) {
if (NotificationService.lastNotificationLoopLDT != null) {
if (clearedAt.isAfter(NotificationService.lastNotificationLoopLDT) amp;amp;
clearedAt.isBefore(LocalDateTime.now())) {
//fire the notification
try {
fireSubClearedNotification(sub);
} catch (Exception ex) {
Log.i("Halflife.doWork", ex.toString());
return Result.failure();
}
}
}
}
}
} else {
GlobalMisc.debugMsg("NotificationWorker:doWork",
"Would have just gone into substance clearing notification loop");
}
} else {
Log.i("Halflife.notificationLoop", "In notification debugging "
"mode");
try {
fireTaperNotification(null);
} catch (Exception ex) {
Log.i("Halflife.doWork", ex.toString());
return Result.failure();
}
}
NotificationService.lastNotificationLoopLDT = LocalDateTime.now();
return Result.success();
}
/**
* Method handles the actual building of the notification regarding
* the applicable taper, and shows it unless our handy HashMap
* 'firedNotifications' shows that there is already a notification
* present for this particular taper.
*
* @param taper the taper to display notification for
*/
private void fireTaperNotification(Taper taper) throws Exception {
Context ctxt = getApplicationContext();
float currentDosageScheduled = taper.findCurrentScheduledDosageAmount();
//here's the legitimate meat 'n potatoes for firing a notification
try {
//if we've already blown the dosage required for the next administration, just skip this
//one
if (currentDosageScheduled <= 0) {
Log.d("fireTaperNotification", "More dosage taken than needs to be "
"for the current taper step; skipping this taper administration.");
return;
}
Intent intent = new Intent(ctxt, AdminData.class);
intent.putExtra("SUB_NDX",
GlobalMisc.getSubListPositionBySid(taper.getSid()));
intent.putExtra("NOTIFICATION_BASED", true);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctxt);
stackBuilder.addParentStack(SubData.class);
stackBuilder.addNextIntentWithParentStack(intent);
Intent delIntent = new Intent(ctxt, NotificationDismissalReceiver.class);
delIntent.putExtra("TAPER", true);
delIntent.putExtra("SUB_ID", taper.getSid());
PendingIntent pendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent pendingDelIntent = PendingIntent.getBroadcast(ctxt, 0,
delIntent, PendingIntent.FLAG_UPDATE_CURRENT);
LocalDateTime latestUsageLDT;
LocalDateTime todaysOpeningConstraintLDT;
boolean beforeOpeningConstraint = false;
latestUsageLDT = Converters.toLocalDateTime(
Permanence.Admins.getLatestUsageTimestampBySid(taper.getSid()));
if (taper.isConstrained()) {
todaysOpeningConstraintLDT =
LocalDateTime.now().withHour(taper.getStartHour().getHour())
.withMinute(taper.getStartHour().getMinute())
.withSecond(0);
if (latestUsageLDT.plus(taper.getTotalConstraintDuration()).isBefore(
todaysOpeningConstraintLDT)) {
beforeOpeningConstraint = true;
}
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(
ctxt, "halflife")
.setContentTitle("Half-life Timer Taper " taper.getName())
//note that the above line, right after "Due since: " , will
//end up displaying the epoch start date for a taper on a
//substance that has no administrations whatsoever
.setSmallIcon(ctxt.getResources().getIdentifier("plus_medical_blue",
"drawable", ctxt.getPackageName()))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setDeleteIntent(pendingDelIntent)
.setAutoCancel(true);
long rawTimestamp = Permanence.Admins.getLatestUsageTimestampBySid(taper.getSid());
GlobalMisc.debugMsg("fireTaperNotification",
"Permanence.Admins.getLatestUsageTimestampBySid returns: "
rawTimestamp);
if (Converters.toLocalDateTime(rawTimestamp).isBefore(
LocalDateTime.of(1980, 1, 1, 0, 0, 0))) {
builder.setContentText("Due: "
String.format("%.2f", currentDosageScheduled)
Permanence.Subs.getUnitsBySID(taper.getSid()) "/"
Permanence.Subs.loadSubstanceById(
taper.getSid()).getCommon_name() "n"
"Due now");
} else if (beforeOpeningConstraint) {
builder.setContentText("Due:"
currentDosageScheduled
Permanence.Subs.getUnitsBySID(taper.getSid()) " of "
Permanence.Subs.loadSubstanceById(
taper.getSid()).getCommon_name() "n"
"Due since: "
LocalDateTime.now().withHour(taper.getStartHour().getHour())
.withMinute(taper.getStartHour().getMinute())
.withSecond(0));
} else {
builder.setContentText("Due:"
currentDosageScheduled
Permanence.Subs.getUnitsBySID(taper.getSid()) " of "
Permanence.Subs.loadSubstanceById(
taper.getSid()).getCommon_name() "n"
"Due since: "
Converters.toLocalDateTime(
Permanence.Admins.getLatestUsageTimestampBySid(
taper.getSid())).plus(
Duration.ofDays(1).dividedBy(
taper.getAdminsPerDay())));
}
NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(ctxt);
notificationManager.notify(1, builder.build());
if (GlobalMisc.Debugging || notificationDebugging) {
Log.i("Halflife.fireNotification",
"attempted to send taper notification");
}
} catch (Exception ex) {
Log.e("Halflife.fireNotification",
"Something broke in taper notification: " ex.toString());
throw new Exception("taper notification broke");
}
}
private void fireSubClearedNotification(Substance sub) throws Exception {
Context ctxt = getApplicationContext();
try {
Intent intent = new Intent(ctxt,
SubsRankedByLastUsage.class);
PendingIntent pendingIntent = PendingIntent.getActivity(
ctxt, 1, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(
ctxt, "halflife")
.setContentTitle("Half-life Timer Cleared: " sub.getCommon_name())
.setContentText(sub.getCommon_name() " cleared at "
sub.getFullEliminationLDT().toString())
.setSmallIcon(ctxt.getResources().getIdentifier("plus_medical_blue",
"drawable", ctxt.getPackageName()))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(ctxt);
notificationManager.notify(1, builder.build());
if (GlobalMisc.Debugging || notificationDebugging) {
Log.i("Halflife.fireNotification",
"attempted to send sub clearednotification");
}
} catch (Exception ex) {
Log.e("Halflife.fireNotification",
"Something broke in sub cleared notification: " ex.toString());
throw new Exception("sub cleared notification broke");
}
}
}
Я был бы очень признателен всем, кто мог бы дать какое-либо представление о том, почему происходит такое поведение, советы о том, как избежать этого поведения, где найти API и другую документацию по старой, устаревшей библиотеке JetPack work v1.0.1 или что я могу сделать в Android Studio, чтобылучше диагностируйте эту проблему, поскольку попытки отладки с помощью того, что я знаю, как это сделать, оказались тщетными.
Большое вам спасибо за ваше время и помощь в этом вопросе!