Android PeriodicWorkRequest (work v1.0.1 [arch, не androidx]) появляется 20 раз каждые 15 м, а не один раз, почему?

#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, чтобылучше диагностируйте эту проблему, поскольку попытки отладки с помощью того, что я знаю, как это сделать, оказались тщетными.

Большое вам спасибо за ваше время и помощь в этом вопросе!