3

I´ve been facing a issue with WorkManager when reescheduling jobs. Currently, I´ve found that at some point after launching a request with Okhttp and raising an error on AuthInterceptor it gets stuck and no other job gets launched.

This is the JobOrganizer class that manages the first steps of the work planing. It chains the first queue of jobs. You will see more jobs that are not pasted here but the main difference is that the first chained job goes without WiFi network constraints and the others does.

object JobOrganizer {

    const val WORK_INTERVAL: Long = 20
    const val SCH_DATA_UPDATE_WORK_RESCHEDULE = "scheduled_data_update_work_reschedule"
    const val SCH_DATA_UPDATE_WORK = "scheduled_data_update_work"

    private val schDataUpdateJob: OneTimeWorkRequest
        get() = OneTimeWorkRequestBuilder<SCHDataUpdateJob>()
                .addTag(SCH_DATA_UPDATE_WORK)
                .setConstraints(wifiConstraint)
                .build()

    val wifiConstraint: Constraints
        get() = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.UNMETERED)
                .setRequiresDeviceIdle(false)
                .setRequiresBatteryNotLow(false)
                .setRequiresCharging(false)
                .setRequiresStorageNotLow(false)
                .build()

    fun getWorkInfos(context: Context, tag: String): LiveData<List<WorkInfo>> {
        val workManager = WorkManager.getInstance(context)
        return workManager.getWorkInfosByTagLiveData(tag)
    }

    private fun clearWorks(workManager: WorkManager) {
        workManager.pruneWork()
    }

    private fun cancelSCHJobs(context: Context) {
        val workManager = WorkManager.getInstance(context)
        workManager.cancelAllWorkByTag(SCH_DATA_UPDATE_WORK )
        clearWorks(workManager)
    }

    fun scheduleJobs(context: Context) {
        cancelSCHJobs(context)
        WorkManager.getInstance(context)
                .beginWith(schTypesDownloadJob)
                .then(schDownloadJob)
                .then(schDataUpdateJob)
                .then(schDataUploadJob)
                .then(schCleanupJob)
                .enqueue()
        FirebaseAnalytics.getInstance(context).logEvent(AnalyticsEvents.Sync.SYNC_SCH_CONFIGURE_FORM_CLEANUP, Bundle())
    }
}

The AuthInterceptor class

class AuthInterceptor(private val context: Context?) : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {

        val originalRequest = chain.request()

        if (context == null) {
            return chain.proceed(originalRequest)
        }

        val auth = AuthRepository(context).getAuth()
        if (auth.isNullOrEmpty()) {
            return chain.proceed(originalRequest)
        }

        val version = String.format(
                "%s: %s (build %s)",
                BuildConfig.FLAVOR,
                BuildConfig.VERSION_NAME,
                BuildConfig.VERSION_CODE
        )

        val compressedRequest = originalRequest.newBuilder()
                .header("Authorization", String.format("Bearer %s", auth[0].token))
                .header("mobile-app-version", version)
                .build()
        return chain.proceed(compressedRequest)
    }

}

The Update Job that reeschedules itself with a 30 min delay. The main try / catch is for the AuthInterceptor errors.

class SCHDataUpdateJob(var context : Context, params : WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {
        FirebaseAnalytics.getInstance(context).logEvent(AnalyticsEvents.Sync.SYNC_SCH_UPDATE_START, Bundle())
        var success = UPDElementTypesJob(context).doWork()
        if (!success) {
            FirebaseAnalytics.getInstance(context).logEvent(AnalyticsEvents.Sync.SYNC_UPD_ELEMENTTYPES_ERROR, Bundle())
            Log.e("SYNC", "SCHDataUpdateJob UPDElementTypesJob error")
        }
        FirebaseAnalytics.getInstance(context).logEvent(AnalyticsEvents.Sync.SYNC_SCH_UPDATE_FINISH, Bundle())

        val dataUpdateWorkRequest = OneTimeWorkRequestBuilder<SCHDataUpdateJob>()
                .setInitialDelay(JobOrganizer.WORK_INTERVAL, TimeUnit.MINUTES)
                .addTag(JobOrganizer.SCH_DATA_UPDATE_WORK)
                .setConstraints(JobOrganizer.wifiConstraint)
                .build()

        WorkManager.getInstance(applicationContext)
                .enqueue(dataUpdateWorkRequest)

        Log.e("SYNC", "SCHDataUpdateJob finished")
        return Result.success()
    }
}

This is the fragment that calls scheduleJobs().

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    super.onCreateView(inflater, container, savedInstanceState)

    mainView = inflater.inflate(R.layout.fragment_draft_list, container, false)

    sync = mainView!!.findViewById(R.id.sync)

    sync.onClick {
        mFirebaseAnalytics!!.logEvent(AnalyticsEvents.UPDATE_BUTTON_CLICKED, Bundle())
        if (!ConnectionUtils.isConnectedToWifi(activity!!.applicationContext)) {
            showConnectivityDialog()
        } else {
            sync.visibility = View.GONE
            doAsync {
                JobOrganizer.scheduleJobs(context!!)
            }
        }
    }

    if (forceDownload) {
        JobOrganizer.scheduleJobs(context!!)
    }

    return mainView!!
}

At some point the this last job doens't get scheduled or doesn't run. Any clue?

Thanks.

reixa
  • 6,903
  • 6
  • 49
  • 68
  • Where and how do you call `scheduleJobs()` function? Can you show us by updating the question so we can figure it out? – Harry Timothy Mar 03 '20 at 03:39
  • scheduleJobs() gets called just after the login to start the sync proccess, inside a Fragment. You can also call scheduleJobs() by clicking on the update button of that same fragment. Both of them are declared on onCreateView. – reixa Mar 03 '20 at 07:43
  • I think I know the answer, but there is one last condition I need you to verify: if you set a breakpoint at `JobOrganizer.scheduleJobs(context!!)` and `FirebaseAnalytics.getInstance(context)` (inside JobOrganizer) then debug it, are they called? – Harry Timothy Mar 03 '20 at 08:05
  • The first time they get called, both of them. The jobs get fired but at some point they stop being reescheduled. SCHUpdateJob gets itself reescheduled when it succeed. – reixa Mar 03 '20 at 08:23
  • Are you returning any `Result.failure()` anywhere? If that is the case, the other jobs will not run. Maybe you did put a `Result.failure()` when the `AuthInterceptor` returns a false response. However I cannot be certain because you did not share the code where you actually send the request. – Furkan Yurdakul Mar 05 '20 at 13:58
  • 1
    Also please make sure you are not calling `cancelSCHJobs` anywhere in your code while the job sequence is executing. Plus, if you want to reschedule a job any time it needs to execute after 30 minutes, use `PeriodicRequestBuilder` instead of `OneTimeUniqueRequestBuilder` and use a different tag. If the tags on both jobs are the same, there may not be a replacement as you expect. – Furkan Yurdakul Mar 05 '20 at 13:59

1 Answers1

0

Here you can learn how to use work manager.

Create a new project and add WorkManager dependency in app/buid.gradle file

implementation "android.arch.work:work-runtime:1.0.0"

Create a base class of Worker:-

package com.wave.workmanagerexample;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
/**
 * Created on : Mar 26, 2019
 * Author     : AndroidWave
 */
public class NotificationWorker extends Worker {
    private static final String WORK_RESULT = "work_result";
    public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }
    @NonNull
    @Override
    public Result doWork() {
        Data taskData = getInputData();
        String taskDataString = taskData.getString(MainActivity.MESSAGE_STATUS);
        showNotification("WorkManager", taskDataString != null ? taskDataString : "Message has been Sent");
        Data outputData = new Data.Builder().putString(WORK_RESULT, "Jobs Finished").build();
        return Result.success(outputData);
    }
    private void showNotification(String task, String desc) {
        NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
        String channelId = "task_channel";
        String channelName = "task_name";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new
                    NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
        }
        NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channelId)
                .setContentTitle(task)
                .setContentText(desc)
                .setSmallIcon(R.mipmap.ic_launcher);
        manager.notify(1, builder.build());
    }
}

Create WorkRequest:-

Let’s move to MainActivity and create a WorkRequest to execute the work that we just created. Now first we will create WorkManager. This work manager will enqueue and manage our work request.

 WorkManager mWorkManager = WorkManager.getInstance();

Now we will create OneTimeWorkRequest, because I want to create a task that will be executed just once.

OneTimeWorkRequest mRequest = new OneTimeWorkRequest.Builder(NotificationWorker.class).build();

Using this code we built work request, that will be executed one time only

Enqueue the request with WorkManager:-

btnSend.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mWorkManager.enqueue(mRequest);
        }
    });

Fetch the particular task status:-

mWorkManager.getWorkInfoByIdLiveData(mRequest.getId()).observe(this, new Observer<WorkInfo>() {
        @Override
        public void onChanged(@Nullable WorkInfo workInfo) {
            if (workInfo != null) {
                WorkInfo.State state = workInfo.getState();
                tvStatus.append(state.toString() + "\n");
            }
        }
    });

Finally, MainActivity looks like this.

package com.wave.workmanagerexample;
import android.arch.lifecycle.Observer;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
public class MainActivity extends AppCompatActivity {
    public static final String MESSAGE_STATUS = "message_status";
    TextView tvStatus;
    Button btnSend;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvStatus = findViewById(R.id.tvStatus);
        btnSend = findViewById(R.id.btnSend);
        final WorkManager mWorkManager = WorkManager.getInstance();
        final OneTimeWorkRequest mRequest = new OneTimeWorkRequest.Builder(NotificationWorker.class).build();
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWorkManager.enqueue(mRequest);
            }
        });
        mWorkManager.getWorkInfoByIdLiveData(mRequest.getId()).observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(@Nullable WorkInfo workInfo) {
                if (workInfo != null) {
                    WorkInfo.State state = workInfo.getState();
                    tvStatus.append(state.toString() + "\n");
                }
            }
        });
    }
}