I'm working on an app that runs a background service to range bluetooth beacons in intervals.
I start a ForegroundService with a timer to start ranging beacons for 10 seconds every minute, with an interval of 200 millis, calculates the strongest beacon and submits it to the backend API.
This works neatly while the app is in foreground, and, when the screen is off, as long as i'm hooked up using adb logcat. As soon as I take it off, nothing gets submitted to the servers anymore, meaning that no beacons are being ranged anymore.
Here are the relevant code pieces, I hope I didn't simplify too much:
class BeaconService : Service(), BeaconConsumer {
private var beaconManager: BeaconManager? = null
private var rangingTimer = Timer("rangingTimer", true)
private val region = Region("com.beacon.test", Identifier.parse("f7826da6-4fa2-4e98-8024-bc5b71e0893e"), null, null)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
override fun onCreate() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = MyNotificationManager.getInstance()
val notification = notificationManager.buildBeaconServiceNotification(this, "iBeacon service", null)
startForeground(NOTIFICATION_ID, notification)
}
initBeaconManager()
}
private fun initBeaconManager() {
BeaconManager.setDebug(true)
beaconManager = BeaconManager.getInstanceForApplication(this)
beaconManager?.foregroundScanPeriod = 200L
beaconManager?.beaconParsers?.add(BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"))
beaconManager?.bind(this)
}
override fun onBeaconServiceConnect() {
beaconManager?.addRangeNotifier { beacons, _ ->
if (beacons.isNotEmpty()) {
//code add ranged beacons to list
}
}
startRanging()
}
private fun startRanging() {
//code to reset the list of ranged beacons
beaconManager?.startRangingBeaconsInRegion(region)
rangingTimer.schedule(10000L) {
stopRanging(50000L)
}
}
private fun stopRanging(restartRangingAfter: Long? = null) {
beaconManager?.stopRangingBeaconsInRegion(region)
//code calcuate the strongest beacon and submit to server
if (restartRangingAfter != null) {
rangingTimer.schedule(restartRangingAfter) {
startRanging()
}
}
}
}
On OS Versions 8+, Android limits background processing unless it is part of a foreground service or a job initiated by the JobScheduler. As a result of this limitation, the Android Beacon Library will by default use the JobScheduler on Android 8+. In the foreground, an "immediate" ScanJob will run constantly to do scans. In the background (meaning when no activities are visible with the screen unlocked), Android does not allow this. A job may be scheduled at most once every ~15 minutes. This is why you see scans stop.
It does not matter that you have your own foreground service. Android still enforces these limits on any background processing performed outside that foreground service.
Two alternatives:
Live with the job limitations (scan once every 15 minutes). Use BackgroundPowerSaver to auto switch between foreground/background mode and set beaconManager.setBackgroundScanPeriod(5000) (for a 5 second scan every 15 minutes.) For clarity, you should also set beaconManager.setBackgroundBetweenScanPeriod(15*60*1000) (15 minutes), although you can set a lower value that will be disallowed by the OS on Android 8+.
Set up the library to scan with its own Foreground Service (yes as second foreground service) as described here. You can then stop using your own foreground service, or keep it. If you keep it, you will see two notification icons about the two foreground services running. It is possible to combine those two notifications if you want to keep two foreground services and show just one notification.
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
My app performs quite simple task - it collects cellInfo, extracts cid number of cell tower and put it in main activity when there's a text view with some kind of log of cell ids. Each cellid collects one time per 2 seconds. This function is stored in Intent Service and also there is a notification that appears every time user starts logging.
Recently I found out that despite the fact that there is a Thread.sleep(1000L) in do-while loop, some smartphones do not keep service working correctly. I.e. when I turn off screen, an Intent Service stops working however notification is still present. Sometimes I noticed that some phones run function of getting cellids much slower ignoring sleep method. But when I launch an app with charger plugged in everything works fine on every phone. Not sure it is all about charging but I need delay to be the same on multiple phones.
Here is what I tried.
Works at different speeds on different phones
fun counter(bc: Intent){
val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
var quit = 0
var i = 0
do {
i += 1
quit = Trigger.getTrigger() // companion object
val num_cid = getcids(tm) // function that takes cell tower cid (int)
bc.putExtra(PARAM_OUT_MSG, num_cid .toString()) // sending cid to broadcastIntent
sendBroadcast(bc)
Thread.sleep(1000)
}
while (quit == 0)
}
Works better but notification does not appear.
fun counter(bc: Intent){
val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
Handler().postDelayed(object : Runnable {
override fun run() {
if (Trigger.getTrigger() == 0) {
i += 1
val num_cid = getcids(tm)
bc.putExtra(CURRENT_CID, num_cid .toString())
sendBroadcast(bc)
handler.postDelayed(this, 1000)
}
}
}, 0)
}
What should be done here to have same speeds on every phone and notification presented?
So, in recent system updates Google especially made restriction on background services. System will eventually kill your app, if it only runs background service. To overcome this limitations foreground services or Job Scheduler are used.
I'm trying to get my app working on Android 9. The following code works fine up to Android 8, but for some reason, the JobService does not get rescheduled on android 9. It gets scheduled the first time but does not get rescheduled according to the set periodic.
class RetrieveJobService : JobService() {
override fun onStartJob(params: JobParameters): Boolean {
doBackgroundWork(params)
return true
}
private fun doBackgroundWork(params: JobParameters) {
Thread {
try {
doRetrieveBackgroundStuff(this)
jobFinished(params, false)
} catch (e: Exception) {
jobFinished(params, false)
}
}.start()
}
override fun onStopJob(params: JobParameters): Boolean {
return false
}
}
And here my JobInfo.Builder
val builder = JobInfo.Builder(jobID, componentName)
.setPersisted(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setPeriodic(millis, 15 * 60 * 1000) //15 min
} else {
builder.setPeriodic(millis)
}
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
val scheduler = context.getSystemService(JOB_SCHEDULER_SERVICE) as
JobScheduler
val resultCode = scheduler.schedule(builder.build())
Any ideas?
EDIT: Just to be clear, this code worked fine on Android 8 and below and does also work on the Android Studio emulator running Android 9. As far as I can test, it does not work on any physic device running Android 9.
If you go through THE LINK, you will find:
Unfortunately, some devices implement killing the app from the recents menu as a force stop. Stock Android does not do this. When an app is force stopped, it cannot execute jobs, receive alarms or broadcasts, etc. So unfortunately, it's infeasible for us to address it - the problem lies in the OS and there is no workaround.
It is a known issue. To save battery, many manufacturers force close the app, thus cancelling all the period tasks, alarms and broadcast recievers etc. Major manufacturers being OnePlus(you have option to toogle),Redmi,Vivo,Oppo,Huwaei.
UPDATE
Each of these devices have AutoStartManagers/AutoLaunch/StartManager type of optimization managers. Which prevent the background activities to start again. You will have to manually ask the user to whitelist your application, so that app can auto start its background processess. Follow THIS and THIS link, for more info.
The methods to add to whitelist for different manufactures are give in this stackoverflow answer. Even after adding to whitelist, your app might not work because of DOZE Mode, for that you will have to ignore battery otimizations
Also in case you might be wondering, apps like Gmail/Hangout/WhatsApp/Slack/LinkedIn etc are already whitelisted by these AutoStart Managers. Hence, there is no effect on their background processes. You always receive timely updates & notifications.
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 used the android beacon library to do the following action:
I switched on and off with a fast pace. On and off, on and off, and so on for 8-9 times.
However, the beacon then lost the signal for about 10 seconds and then the signal started to be received again.
Also, I tried an Android API function, "lescan", which resulted in the same situation.
Does anyone know why this happens?
MY testing device is:
HUAWEI P20 Pro 8.1
Samsung S6 7.0
override fun onResume() {
beaconManager = BeaconManager.getInstanceForApplication(this)
beaconManager.getBeaconParsers().add(BeaconParser().
setBeaconLayout(IBEACON_LAYOUT))
beaconManager.getBeaconParsers().add(BeaconParser().
setBeaconLayout(EDDYSTONE_UID_LAYOUT))
beaconManager.getBeaconParsers().add(BeaconParser().
setBeaconLayout(EDDYSTONE_URL_LAYOUT))
beaconManager.getBeaconParsers().add(BeaconParser().
setBeaconLayout(EDDYSTONE_TLM_LAYOUT))
beaconManager.bind(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissions = ArrayList<String>()
if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)) permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION)
if (permissions.size != 0) {
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 100)
}
}
}
override fun onBeaconServiceConnect() {
beaconManager.addRangeNotifier{ beacons,region ->
Log.d("addRangeNotifier",beacons.size.toString())
}
try {
beaconManager.startRangingBeaconsInRegion(Region("com.gigabyte.testkotlin", null, null, null))
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onPause() {
super.onPause()
beaconManager.unbind(this)
}
It's hard to say exactly what you are witnessing without seeing exact code to reproduce, but turning scanning on and off quickly is not necessarily a problem on all devices.
By default, the Android Beacon Library uses a foreground scan period of 1100 ms and a between scan period of 0ms, so it effectively turns scanning on and off 9 times in just over 10 seconds -- similar to what you describe.
I have never noticed these symptoms in normal use of the library on Samsung devices or the Huawei P9, so something else must be triggering this behaviour in your test case.
EDIT: The posted code indicates that the activity itself is what is started and stopped rapidly, and because it binds and unbinds to the beaconManager as it starts and stops, it also starts and stops the Android service that scans for beacons. These are heavy weight data structures that are not designed to be started and stopped rapidly. Short answer: don't do this. If you really need to start and stop your activity rapidly, bind to the beaconManager outside the activity lifcycle, perhaps only one at app startup in the onCreate method of a custom Android Application class.