Madhusudan Sharma
Madhusudan Sharma

Reputation: 443

WorkManager not repeating the PeriodicWorkRequest

I am creating an android application to run my code in background. I'm well aware of the restriction introduced by the Android Oreo for background services and that's why I'm using WorkManager API to schedule the task for the execution. I'm testing my code on Mi Max device with Android API 24 (Nougat) and also enable the auto start manually so that MIUI allows the app to run in background but the problem is, the WorkManager fires for the first time the application starts but after that, it doesn't work. Below is my code I'm using for the periodic work request and work itself.

PeriodicWorkRequest call:

PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(ClassExtendingWorker.class, 15, TimeUnit.MINUTES)
            .setConstraints(Constraints.NONE)
            .build();
WorkManager.getInstance().enqueue(work);

ClassExtendingWorker:

public Result doWork() {
    /*--- SHOWING NOTIFICATION AS AN EXAMPLE TASK TO BE EXECUTED ---*/
    NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getApplicationContext(), "")
            .setSmallIcon(R.drawable.common_google_signin_btn_icon_light)
            .setContentTitle("TestApp")
            .setContentText("Code executed")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT);
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext());
    notificationManager.notify(1234, mBuilder.build());

    return Result.SUCCESS;
}

Upvotes: 23

Views: 32106

Answers (6)

mxkmn
mxkmn

Reputation: 511

I couldn't find a question that I could answer, so I'm leaving my observations here, under the most popular post among those in which the author has encountered problems with periodic work. Yes, the question doesn't specify flexInterval, but I hope my answer will help someone else.

tl;dr: NEVER set flexInterval in PeriodicWorkRequestBuilder and NEVER use OneTimeWorkRequestBuilder with setInitialDelay.

I hated WorkManager in last month and almost switched to AlarmManager. I had a long laugh when work fired after 2 weeks I installed the app, even though it should have within a few hours of installation. I even created a meme because of this. Not so funny when you spend so much time with such a simple library.

Finally, Undefined Behaviour on modern systems using modern languages and tools. I didn't understand why the job wasn't working on modern Android (worked on 5.1 and 7.1, didn't work on 10 and 12).

Debugging started when I switched to OneTimeWorkRequestBuilder on advice from StackOverflow. Something seemed to have changed, but the quality of scheduling was still poor. Next I went back to PeriodicWorkRequestBuilder and started disabling everything to reach the truth. So, after 3 weeks of debugging, I found the problem: it turns out that when flexInterval is set, the job stops performing. This is also a problem on older versions of WorkManager, so maybe it's not a bug in the eyes of Google.

This is a major point in my narrative. And yeah, my flexInterval was more than the minimum interval of 15 minutes (it was 30 minutes).

However, you should also realise that:

  • You need to override getForegroundInfo() in your Worker class, otherwise work will be crashed on devices with Android lower than 12 if .setExpedited() is used.

  • Minimum repeatInterval is 15 minutes.

  • Modern android automatically puts the app into restricted bucket after a few days if it's not opened, and this prohibits the app from running JobScheduler (which is used by the WorkManager). If the user expects constant work from your app, then you should give them the option to disable this optimization. Users should go to the battery optimization settings or you should provide them small dialog (which requires ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS in the manifest, check acceptable cases for this permission). Use my code to do this action:

// check in onResume and show "disable optimizations" button for user if it is false:
val isBatteryOptimizationsDisabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
    (getSystemService(POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName)

// use to disable optimizations:
@SuppressLint("BatteryLife")
fun Context.setUnrestrictedBattery() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
        (getSystemService(POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName)
    ) {
        return
    }

    try {
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            error("")
        }

        startActivity(
            Intent(
                Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
                Uri.parse("package:$packageName"),
            ),
        )
    } catch (_: Exception) {
        toast(getString(R.string.set_unrestricted_battery_permission_via_settings, getString(R.string.app_name))) // add to strings.xml: "Disable optimizations for %s app"
        startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
    }
}
  • App Hibernation extends restricted bucket features, even on old androids (6.0+) with Google Services. To disable it, use this:
// check in onResume and show "disable permission removal" button for user if isPermissionsRevokingAutomatically is true:
PackageManagerCompat.getUnusedAppRestrictionsStatus(applicationContext).apply {
    addListener({
        val isPermissionsRevokingAutomatically = when (this.get()) {
            UnusedAppRestrictionsConstants.API_30_BACKPORT,
            UnusedAppRestrictionsConstants.API_30,
            UnusedAppRestrictionsConstants.API_31,
            -> true
            else -> false
        }
        // show button if true
    }, ContextCompat.getMainExecutor(applicationContext))
}

// add to onCreate and then use disablePermissionsAutodisabling() in button onClick:
val defaultActivityLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {}

val disablePermissionsAutodisabling = {
    defaultActivityLauncher.launch(
        IntentCompat.createManageUnusedAppRestrictionsIntent(applicationContext, packageName),
    )
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
        toast("Find the \"Remove Permissions\" switch and turn it off.")
        toast("It can be found on the Permissions tab.")
    }
}

Tested with WorkManager 2.9.0 and on these firmwares and Android versions:

  • AOSP (including Pixel, Nexus, Sony, HTC devices, Android Go and custom ROMs): 14, 13, 12, 10, 8.0, 7.1, 6.0;
  • OneUI: 13 (OneUI 5.1);
  • Realme UI Global: 12 (Realme UI 3.0);
  • Flyme China (users also need to allow running in background via Security app): 10 (Flyme 9), 5.1 (Flyme 6);
  • MIUI Global (users should go to Settings -> applications -> permissions -> autorun in the background -> enable your application (works on some devices) OR not remove your app from the Task Manager (also they can "lock" it) (always works if the background application settings have not been changed by the user), nothing else helped): 13/12 (MIUI 14), 10 (MIUI 12), 9 (MIUI 11);
  • EMUI Global (users also need to whitelist the application via Settings->Battery->App launch->More (three dots menu)->Manage manually): 10 (EMUI 12).

If your firmware not in list, ask google or check Don't kill my app!.

You should also be aware that on different versions of Android WorkManager's behaviour is slightly different:

  • I noticed that the periodic work on <6.0 (works via AlarmManager) happens only after unlocking the device and does not fire in standby mode (maybe it's only on Flyme 6 firmware);
  • I've also seen that on very modern versions of Android (I didn't even notice this on Android 10, but it works on Android 13), work automatically adjusts its execution time and triggers at about the same time, so there's no point in frequently rescheduling work to trigger at about the same time. Take a look at this screenshot showing the variable trigger time without additional rescheduling. The app was running on Android 13 with OneUI 5.1 and was not running for 10 days. WorkManager is configured to trigger every 8 hours with no flex interval.

If you need to take a look at my code, this is how I call scheduling a periodic work:

fun enqueueCalendarSyncIfNeeded() {
    workManager.enqueueUniquePeriodicWork(
        UNIQUE_PERIODIC_WORK_NAME,
        ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
        PeriodicWorkRequestBuilder<SyncWorker>(8, TimeUnit.HOURS)
            .setConstraints(
                Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.NOT_ROAMING)
                    .build(),
            )
            .setInitialDelay(minutesToNextWork(), TimeUnit.MINUTES)
            .build(),
    )
}

Also, in my application, the WorkManager instance is passed inside the classes using Hilt. I recommend not to use the default WorkManager initialization (by default it uses the AppStartup library for this) if you have DI framework in your project, as AppStartup is only needed for pre-initialization, which is also available there.

To use WorkManager with Hilt, add this to the project:

@Module
@InstallIn(SingletonComponent::class)
object Di {
    @InstallIn(SingletonComponent::class)
    @EntryPoint
    interface WorkManagerInitializerEntryPoint {
        fun hiltWorkerFactory(): HiltWorkerFactory
    }

    @Provides
    @Singleton
    internal fun provideWorkManager(
        @ApplicationContext context: Context,
    ): WorkManager {
        val entryPoint = EntryPointAccessors.fromApplication(
            context,
            WorkManagerInitializerEntryPoint::class.java,
        )

        val configuration = Configuration.Builder()
            .setWorkerFactory(entryPoint.hiltWorkerFactory())
//          .setMinimumLoggingLevel(Log.INFO)
            .build()

        WorkManager.initialize(context, configuration)
        return WorkManager.getInstance(context)
    }
}

And don't forget to disable standard initialization through the AndroidManifest inside the Application description:

    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">

        <meta-data
            android:name="androidx.work.WorkManagerInitializer"
            android:value="androidx.startup"
            tools:node="remove" />
    </provider>

Upvotes: 8

Dibyendu Mahata
Dibyendu Mahata

Reputation: 149

I have solved the problem by changing these codes in the Work Manager.

  1. Use try & catch block in the Worker class to get the Result.success() and Result.failure()
  2. Change the ExistingPeriodicWorkPolicy from KEEP to UPDATE.

Upvotes: 0

Sylvester Yao
Sylvester Yao

Reputation: 295

If you use PeriodicWorkRequest with interval less than 15 min, you should return Result.retry(), not success or failure.

Upvotes: 13

Abhilash Das
Abhilash Das

Reputation: 1438

according to the documentation:

 /**
 * The minimum interval duration for {@link PeriodicWorkRequest} (in milliseconds).
 */
public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes.
/**
 * The minimum flex duration for {@link PeriodicWorkRequest} (in milliseconds).
 */
public static final long MIN_PERIODIC_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes.

if you are setting this two value less than min specified, then you have to wait approx 20 min to see the log/output

Upvotes: 25

karmil32
karmil32

Reputation: 133

Add flex interval and flex unit to builder constructor. In my case that's started to work.

PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(ClassExtendingWorker.class, 15, TimeUnit.MINUTES, 5, TimeUnit.MINUTES)

Upvotes: 2

Viraj Patel
Viraj Patel

Reputation: 2393

Update your WorkManager version to 1.0.0-alpha04. You can check release notes here

Also, refer to this PeriodicWorkRequest GitHub demo and update TimeUnit as per your requirement in DayIncrementViewModel.java. It will work as per your need.

WorkManager is still in alpha mode so it will work completely for all devices once they release the final version.

Upvotes: 6

Related Questions