maybe this is yet another question about WorkManager, but I really can't find a solution...
I'm trying, as the title suggests, to run a periodic work every 15 minutes. Actually in the worker I'm polling some data every minute. After every poll, every almost 1 second I check if the worker is being stopped and if so return, otherwise keep waiting until 1 minute is reached and poll data again.
According to the documentation this should work and indeed it is, until I kill the app from the recent app screen.
Here is the code:
package com.qsea.app.cordova;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class ServerListenerWorker extends Worker {
public static final String TAG = "ServerListenerWorker";
public ServerListenerWorker(
Context appContext,
WorkerParameters workerParams
) {
super(appContext, workerParams);
}
#Override
public Result doWork() {
Log.d(TAG, "Doing work");
final long startTime = SystemClock.elapsedRealtime();
final int maxDelta = 840000; // 14 minutes
while (true) {
// I did this to stop this worker until 15 minutes
// and then let the next worker run
if (SystemClock.elapsedRealtime() - startTime >= maxDelta) {
break;
}
// Here I'm polling data, if the polling results in a failure
// I return Result.retry()
// It avoid waiting if it remains only 1 minute until the max time
if (SystemClock.elapsedRealtime() - startTime >= (maxDelta - 60000)) {
break;
}
for (int i = 0; i < 60; i++) {
SystemClock.sleep(950);
// Here it checks if it is stopped
if (isStopped()) {
Log.d(TAG, "Detected stop"); // this is actually never reached
break;
}
}
}
return Result.success();
}
}
And I do as following to start the work
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(ServerListenerWorker.class, 15, TimeUnit.MINUTES)
.addTag(serverListenerWorkerUID)
.setConstraints(constraints)
.build();
WorkManager.getInstance(cordova.getActivity()).enqueueUniquePeriodicWork(serverListenerWorkerUID, ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest);
I check the correct working of the worker by viewing the log of my server (If I receive the request it is working fine) and it seems to work ok until I close the app. Then it stops running the actual worker and never run a worker again until I reopen the app, where the work seems to be enqueued and resumes right after the app opening.
Am I doing something wrong in the initialization?
I have also <service android:name=".ServerListenerWorker" android:permission="android.permission.BIND_JOB_SERVICE" /> in my AndroidManifest.xml
Is this expected behavior?
I read that chinese ROM have additional restriction to background services and I have a HUAWEI, which seems to be the worst in this. So could it be the device? And if so, how do Facebook, WhatsApp, Instagram and others manage this to work even in these devices?
Let's split this in two different problems.
What are you doing in your workers is battery heavy and would be better if you use a foreground service that notifies the user that your application is running continuously in the background. You can also use WorkManager with the newly introduced support for long-running workers (that under the hood uses a foreground service). More on this in the documentation.
If have a problem with a specific OEMs, please open an issue on the Android issuetracker as this maybe a CDD violation. Google can contact the OEM and request that they fix the ROM. This is going to take time, in the meanwhile, you can take a look at sites like don't kill my app to understand what are the constraints on a specific device and use a library like autostarter to help the user to navigate to the right setting.
BTW, you don't need to be list your workers in the AndroidManifest.xml file
Related
I'm working on an Android app with a constant repeating background process.
From the moment the device starts it should load data off a webpage every minute. It uses XmlPullParser and a simple URL inputstream. It is but 10kb so it isn't that intensive. I believe this kind of task is called Deferred. The information loaded by the process has to be accessible to the Activity once that the user opens the app. The background process also needs to be abled to place a notification once the data shows certain results.
There seem to be multiple methods to achieve this in Android, eg. a JobScheduler, WorkManager or AlarmManager however everything I've tried so far seems to either stop once the activity closes or doesn't run at all. The timing, every minute, also seems to be an issue as for both a repeating job and worker the minimum interval is 15. This one minute doesn't have to be exact. I imagine instead of having a repeating process loading the data once it might be better to have a long running process sleeping for 1m in between loading the data.
I do not have access to the server the application is connecting to. so I can't do a FirebaseMessagingService.
What would be the best way to schedule such a background process?
How can the activity best exchange information with that process?
I'm open for all suggestions,
thank you for your time.
Easy with WorkManager, it's the most encouraged way for Scheduling Repeating background work in Android, see introduction.
As you say, the minimum repeating work request interval is restricted to 15 minutes, the only way to break it is to Repeatedly schedule the one-time work.
1. Setup Your Worker Class:
class ToastShower(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
withContext(Dispatchers.Main) { //ui related work must run in Main thread!!
Toast.makeText(applicationContext, "Hey, I'm Sam! This message will appear every 5 seconds.", Toast.LENGTH_SHORT).show()
}
return Result.success()
}
}
2. Setup Your Custom Application Class:
class WorkManagerApplication : Application() {
private val backgroundScope = CoroutineScope(Dispatchers.Default) //standard background thread
private val applicationContext = this
override fun onCreate() { //called when the app launches (same as Activity)
super.onCreate()
initWork()
}
private fun initWork() {
backgroundScope.launch { //all rnu in background thread
setupToastShowingWork(0) //no delay at first time
observeToastShowingWork() //observe work state changes, see below
}
}
private fun setupToastShowingWork(delayInSeconds: Long) { //must run in background thread
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) //when using WiFi
.build()
val oneTimeRequest = OneTimeWorkRequestBuilder<ToastShower>() //【for breaking 15 minutes limit we have to use one time request】
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS) //customizable delay (interval) time
.setConstraints(constraints)
.build()
WorkManager.getInstance(applicationContext).enqueueUniqueWork( //【must be unique!!】
ToastShower::class.java.simpleName, //work name, use class name for convenient
ExistingWorkPolicy.KEEP, //if new work comes in with same name, discard the new one
oneTimeRequest
)
}
private suspend fun observeToastShowingWork() {
withContext(Dispatchers.Main) { //must run in Main thread for using observeForever
WorkManager.getInstance(applicationContext).getWorkInfosForUniqueWorkLiveData(ToastShower::class.java.simpleName).observeForever {
if (it[0].state == WorkInfo.State.SUCCEEDED) { //when the work is done
backgroundScope.launch { //prevent from running in Main thread
setupToastShowingWork(5) //every 5 seconds
}
}
}
}
}
}
3. Setup AndroidManifest File:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.workmanagertest">
<application
android:name=".WorkManagerApplication" //【here, must!!!】
...
</application>
</manifest>
By setting up with above, the work (showing Toast in my example) will be executed (or more clearly, schedule and execute) every 5 seconds no matter the app is in foreground or background or killed by system. Only way to stop it is either uninstall or go inside the app's setting to force-close it.
Demo: https://youtu.be/7IsQQppKqFs
I have two periodic workers scheduled in my app where one worker repeats after 24 hours and another in 15 minutes.
Initially on fresh install things work as expected, but after some days I got an issue on 2 devices(out of 5).
The 24 hour worker is triggered properly but the 15 minute one isn't triggered at all. I have been monitoring this for 24 hours now.
I viewed the databse of workmanager via Stetho and saw some entries for 24-hour worker and 0 entries for 15 minute worker. I'm looking in the WorkSpec table.
I debugged via Android studio and after querying WorkManager using getWorkInfosByTag() I got a list of 80 objects for the 15-minute worker where 79 were in CANCELED state and one was in ENQUEUED state.
So apparently, canceled workers are not added to the DB?
I did not find any document from Google which explains the scenarios in which worker is canceled.
I am using 1.0.0-beta03 version of the work runtime.
Also, I am not killing the app or doing anything funny. The app is running in the background and not being killed.
Devices are Mi A2 (Android 9), Redmi Note 4(Android 7).
I need to understand why is the worker being canceled and is there any better way to debug this? Any pointers will be helpful and upvoted!
Thanks.
Edit1: Posting the code to schedule both workers.
24-hour periodic worker:
public static synchronized void scheduleWork() {
checkPreviousWorkerStatus();
if (isWorking()) {
Log.i("AppDataCleanupWorker", "Did not schedule data cleanup work; already running.");
return;
}
if (lastWorkId != null) {
WorkManager.getInstance().cancelAllWorkByTag("AppDataCleanupWorker");
lastWorkId = null;
}
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(true)
.build();
PeriodicWorkRequest.Builder builder = new PeriodicWorkRequest
.Builder(AppDataCleanupWorker.class, 24, TimeUnit.HOURS)
.addTag("AppDataCleanupWorker")
.setConstraints(constraints);
PeriodicWorkRequest workRequest = builder.build();
lastWorkId = workRequest.getId();
WorkManager.getInstance().enqueue(workRequest);
List<WorkInfo> workInfos = WorkManager.getInstance()
.getWorkInfosByTagLiveData("AppDataCleanupWorker")
.getValue();
if (workInfos != null && workInfos.size() > 1) {
throw new RuntimeException("Multiple workers scheduled. Only one schedule is expected.");
}
}
15-minute periodic worker:
public static synchronized void scheduleWork() {
checkPreviousWorkerStatus();
if (isWorking) {
Log.i("ImageUploadWorker", "Did not schedule image upload work; already running.");
return;
}
if (lastWorkId != null) {
WorkManager.getInstance().cancelAllWorkByTag("ImageUploadWorker");
lastWorkId = null;
}
Constraints constraints = new Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest.Builder builder =
new PeriodicWorkRequest.Builder(ImageUploadWorker.class, 15,
TimeUnit.MINUTES)
.addTag("ImageUploadWorker")
.setConstraints(constraints);
PeriodicWorkRequest workRequest = builder.build();
lastWorkId = workRequest.getId();
WorkManager.getInstance().enqueue(workRequest);
List<WorkInfo> workInfos = WorkManager.getInstance()
.getWorkInfosByTagLiveData("ImageUploadWorker").getValue();
if (workInfos != null && workInfos.size() > 1) {
throw new RuntimeException("Multiple workers scheduled. Only one schedule is expected.");
}
}
Note: The device is connected to the Internet & network speed is pretty good.
SOLVED: Worker not being triggered by WorkManager
Resolved the issue after some debugging. Posting here in case someone runs into the same issue.
So, I was canceling and enqueuing workers again and again. So lets say a worker is scheduled for 11.15 AM today, then I cancel and enqueue again, the 11.15 AM slot was not being given to the newly enqueued worker.
Instead, When the 11.15 AM slot is utilised, the work manager just checks that the scheduled worker was canceled and does not trigger the newly enqueued worker.
This was the behaviour on 3 out of 5 devices we tested on. On 2 devices the newly enqueued worker was properly being triggered.
Now the solution:
Remove all code to schedule your workers.
In the onCreate() of your application, first invoke pruneWork() on WorkManager to remove all piled up cancelled worker schedules. Remember the method returns Operation which will help you check the completion of removal.
Before calling pruneWork() you might also call cancelAllWorkByTag() for all your workers to clean up any and all the pending schedules. This method also returns an Operation.
After the work manager schedules are cleared, you can now schedule your PeriodicWorkRequest the way you want. I used enqueueUniquePeriodicWork() to make sure only one instance of worker is running at a time.
Now, my worker is being triggered every 15 minutes properly.
Note that as and when your device sleeps and goes into doze mode, this 15 minute duration will increase.
You can check the work manager database using Stetho library.
The table name is WorkSpec where you'll find all the schedules for your workers. And you can stop app execution at some breakpoint and use getWorkInfosByTag() on WorkManager to get a list of schedules and their current status.
You're doing a few things that are incorrect.
You're using LiveData and calling getValue() on it without adding an Observer. This won't give you what you're looking for - the LiveData never starts tracking the values that you want. Please check out proper LiveData usage here: https://developer.android.com/topic/libraries/architecture/livedata
If you only want one particular copy of a type of work, you should use enqueueUniqueWork instead of enqueue.
Unless you found yourself in an extremely bad situation where you actually need to remove old workers, I would advise you not to call pruneWork(). Please see the documentation: https://developer.android.com/reference/androidx/work/WorkManager#pruneWork()
I have an app that should show a notification every 2 hours and should stop if user has already acted upon the notif. Since background services are history now, I thought of using WorkManager ("android.arch.work:work-runtime:1.0.0-beta01") for the same.
My problem is that although the work manager is successfully showing the notifications when app is running, but it won't show notification consistently in the following cases(I reduced the time span from 2 hours to 2 minutes to check the consistency):
when app is killed from the background.
device is in screen off.
state device is in unplugged state(i.e not charging).
By consistency , i mean that the notifications show at least once in the given time span. for 2 minutes time span, the freq of notifications went from once every 4 minutes to completely not show any notification at all. for 2 hours timespan( the timespan that i actually want), its been 4 hours and i haven't got a single notification. Here is the Code i am using for calling WorkManger:
public class CurrentStreakActivity extends AppCompatActivity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
setDailyNotifier();
...
}
private void setDailyNotifier() {
Constraints.Builder constraintsBuilder = new Constraints.Builder();
constraintsBuilder.setRequiresBatteryNotLow(false);
constraintsBuilder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
constraintsBuilder.setRequiresCharging(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
constraintsBuilder.setRequiresDeviceIdle(false);
}
Constraints constraints =constraintsBuilder.build();
PeriodicWorkRequest.Builder builder = new PeriodicWorkRequest
.Builder(PeriodicNotifyWorker.class, 2, TimeUnit.HOURS);
builder.setConstraints(constraints);
WorkRequest request = builder.build();
WorkManager.getInstance().enqueue(request);
}
....
}
Here is the worker class(i can post showNotif(..) and setNotificationChannel(...) too if they might be erroronous):
public class PeriodicNotifyWorker extends Worker {
private static final String TAG = "PeriodicNotifyWorker";
public PeriodicNotifyWorker(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
Log.e(TAG, "PeriodicNotifyWorker: constructor called" );
}
#NonNull
#Override
public Result doWork() {
// Log.e(TAG, "doWork: called" );
SharedPreferences sp =
getApplicationContext().getSharedPreferences(Statics.SP_FILENAME, Context.MODE_PRIVATE);
String lastcheckin = sp.getString(Statics.LAST_CHECKIN_DATE_str, Statics.getToday());
// Log.e(TAG, "doWork: checking shared preferences for last checkin:"+lastcheckin );
if (Statics.compareDateStrings(lastcheckin, Statics.getToday()) == -1) {
Log.e(TAG, "doWork: last checkin is smaller than today's date, so calling creating notification" );
return createNotificationWithButtons(sp);
}
else {
Log.e(TAG, "doWork: last checkin is bigger than today's date, so no need for notif" );
return Result.success();
}
}
private Result createNotificationWithButtons(SharedPreferences sp) {
NotificationManager manager =
(NotificationManager) getApplicationContext().getSystemService((NOTIFICATION_SERVICE));
String channel_ID = "100DaysOfCode_ID";
if (manager != null) {
setNotificationChannel(manager,channel_ID);
showNotif(manager, channel_ID, sp);
return Result.success();
}
else {
return Result.failure();
}
I am using a xiaomi miA2 androidOne device with Android Pie(SDK 28). There are a few other things that are troubling me:
What can i possibly do to know if my WorkManager is running? Other that just wait for 2 hours and hope for a notification. I actually tried something like that, keeping my phone connected to pc and checking android studio's logcat every now and then. It DOES run all the logs when the worker is actually called, but i don't think that's a correct way to test it, or is it?
In the above Code, the setDailyNotifier() is called from the onCreate() every time the app is opened. Isn't it Wrong? shouldn't there be some unique id for every WorkRequest and a check function like WorkManger.isRequestRunning(request.getID) which could let us check if a worker is already on the given task??If this was a case of AsyncTask, then boy we would have a mess.
I have also checked #commonsware's answer here about wakelock when screen is off, but i remember that work manager does use alarm manager in the inside when available. So what am I missing here?
Few comments:
WorkManager has a minimum periodic interval of 15minutes and does not guarantee to execute your task at a precise time. You can read more about this on this blog.
All the usual background limitation you've on newer Android releases are still relevant when you use WorkManager to schedule your tasks. WorkManager guarantees that the task are executed even if the app is killed or the device is restated, but it cannot guarantee the exact execution.
There's one note about the tasks being rescheduled when your app is killed. Some OEM have done modification to the OS and the Launcher app that prevents WorkManager to be able to accomplish these functionality.
Here's the issuetracker discussion:
Yes, it's true even when the phone is a Chinese phone.
The only issue that we have come across is the case where some Chinese OEMs treat swipe to dismiss from Recents as a force stop. When that happens, WorkManager will reschedule all pending jobs, next time the app starts up. Given that this is a CDD violation, there is not much more that WorkManager can do given its a client library.
To add to this, if a device manufacturer has decided to modify stock Android to force-stop the app, WorkManager will stop working (as will JobScheduler, alarms, broadcast receivers, etc.). There is no way to work around this. Some device manufacturers do this, unfortunately, so in those cases WorkManager will stop working until the next time the app is launched.
As of now , i have this app installed for last 8 days and i can confirm that the code is correct and app is working fine. as said by pfmaggi , the minimum time interval for work manager to schedule the work is 15 minutes, so there is a less chance that the WorkManager would have worked as expected in my testing conditions( of 2 minutes ) . Here are some of my other observations:
Like I said in the question that i was unable to recieve a notification for 4 hours even though i have passed the repeat interval as 2 hours. This was because of Flex Time. I passed in the flex time of 15 minutes and now it shows notifications between correct time interval. so i will be marking pfmaggi's answer as correct.
The problem of repeated work request can be solved by replacing WorkManager.getInstance().enqueue(request) with WorkManager.getInstance().enqueueUniqueWork(request,..)
I was still unable to find a way to test the work manager in the way i have described.
I have a quite strange issue with a cwac-wakeful library and wakelocks.
In my app, I'm using cwac-wakeful to periodically download a data from a web server and this works very well. But when somewhere in the app I acquire a wakelock (even when the set alarms were cancelled), cwac-wakeful starts to behave a little odd - it starts a job at full hours exactly every 5 minutes (14:00, 14:05...), regardless of set repeating interval.
Releasing the wakelock gives no effect and the only method to fix this is to completely restart the application.
I know that cwac-wakeful takes advantage of wakelocks while performing a job, so maybe the problem is here.
Here is a class that implements the AlarmListener.
public class ServiceWaker implements WakefulIntentService.AlarmListener
{
private static final Logger log = Logger.getLogger(ServiceWaker.class);
public ServiceWaker()
{
log.info("New ServiceWaker");
}
#Override
public void scheduleAlarms(AlarmManager alarmManager, PendingIntent pendingIntent, Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long syncFrequency = Long.parseLong(prefs.getString("sync_frequency", "1"));
syncFrequency = syncFrequency * 60 * 1000;
log.info("Alarm scheduled with a repeat interval:" + syncFrequency);
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime()+60000, syncFrequency, pendingIntent);
}
#Override
public void sendWakefulWork(Context context)
{
log.info("Sent wakeful work");
WakefulIntentService.sendWakefulWork(context, CheckServiceWakeful.class);
}
#Override
public long getMaxAge()
{
return AlarmManager.INTERVAL_FIFTEEN_MINUTES*2;
}
}
Details:
Target SDK: 17
Minimum SDK: 12
Compile SDK: 17
BuildTools version: 19.0.3
Late answer but I forgot to post it after I found out the solution.
The problem was an energy manager in my mobile. I have 3 battery plans available:
Energy saving - runs only essential services
Smart - prolongs the battery life by doing some optimization with wake locks, I have had this plan set to active
Normal - everything runs normally
"Smart" plan was responsible for this issue. As I have read somewhere, when set to active, the system tries to batch wake locks and network tasks, so the mobile is not continuously being waken. Thus my task was being re-scheduled with a longer period, in order to run it with another tasks every - for example - 15 minutes.
I'm trying to implement a convenient-to-use system for handling status bar notifications for android, and i was thinking about the following:
Create a database, where i store when and what to show
Create a service what runs in the background using the 'interval' Service, what the API provides
In that service check if any notification needs to be shown according to the database, then show it.
The only problem is, that, i cannot detect, if i need to start the service or not. I tried these things, but none of them worked well so far:
1.) Save if the service was already started on the local storage:
// Do this on application startup
var isRunning = Ti.App.Properties.getBool("service_running", false);
if(!isRunning)
{
var service = Titanium.Android.createService(...);
service.addEventListener('start', function()
{
Ti.App.Properties.setBool("service_running", true);
});
service.addEventListener('stop', function()
{
Ti.App.Properties.setBool("service_running", false);
});
service.start();
}
This obviously won't work, because the android systems native onStop and onDestroy events will not be dispatched, if the Service doesn't terminates unusually (like the user force stops the app), so the stop event also won't be fired.
2.) Try to access any active service via Titanium.Android.getCurrentService(), but i got an error saying Titanium.Android has no method called getCurrentService(). This is pretty strange, because the IDEs code completion offered me this method.
3.) Use an Intent to clear the previously running Service
var intent = Titanium.Android.createServiceIntent
(
{
url : 'notification/NotificationService.js'
}
);
intent.putExtra('interval', 1000 * 60);
//Stop if needed
Titanium.Android.stopService(intent);
//Then start it
Titanium.Android.startService(intent);
But it seems like i need to have the same instance of Intent, that started the service to stop it, because doing this on application startup, then exiting and restaring it results in multiple Services to run.
At this point i ran out of ideas, on how to check for running services. Please if you know about any way to do this, let me know! Thanks for any hints!
EDIT
Here are the source materials which gave me the idea to try the above methods (maybe only i use them incorrectly):
The local storage: Titanium.App.Properties
The method for accessing running services: Titanium.Android.getCurrentService
The method for stoping a service with an Intent: Titanium.Android.stopService
And the full source for the NotificationHandler "class" and NotificationService.js that I wrote, and their usage: link
Use Bencoding AlarmManager and it will provide all you need to schedule an alarm notification : https://github.com/benbahrenburg/benCoding.AlarmManager
This module provides what you need. It's really easy - just set repeat to daily when sheduling a Notification or Service.
Refer https://gist.github.com/itsamiths/6248106 for fully functional code
I am checking if the service is started then show daily notification or else start service and then show daily notification
var isRunning = Ti.App.Properties.getBool("service_running", false);//get service running bool status
if (isRunning) {
Ti.API.info('service is running');
} else {
Ti.API.info('service is not running');
alarmManager.addAlarmService({
service : 'com.mkamithkumar.whatstoday.DailyEventNotificatoinService',
hour : "08",
repeat : 'daily'
});
}
I come one year late, but maybe this can help others in the future.
We had the same idea: run the service forever and do the checks on every cycle (I must check 20 different communications).
And I had the same problem: how to detect that the service is running, to don't run again to don't duplicate the checks.
To solve that problem, what I did is get the current time on every cycle and save it to store.
Then, before launch a new service, I check if the last execution was to far in time: if true, then the service was stopped, else is running.
Not very elegant, but was the only way I found to avoid the problem of the user killing the app (and the service).
This is my code for the "launcher" of the service. In my case, I test 30 seconds far away:
exports.createAndroidServiceForNotifications = function(seconds) {
var moment = require('alloy/moment');
var diffSeconds = moment().diff(Ti.App.Properties.getString('serviceLastRun', new Date().getTime() - 60000), 'second');
if (diffSeconds > 30) {
var now = new Date().getTime();
var delta = new Date(now + (seconds * 1000));
var deltaMS = delta - now;
var intent = Ti.Android.createServiceIntent({
url : 'notificationsService.js'
});
intent.putExtra('interval', deltaMS);
Ti.Android.startService(intent);
}
};