
I have been working hard these days to find a way to run a service in Android that never stops. This is just a guide pursuing the same goal. Hope it helps you!
Problem
With the introduction of Android battery optimization in Android 8.0 (API level 26), background services now have some significant limitations. Basically, once an app runs in the background for a while, it gets killed, making it worthless for running a service that never stops.
According to Android’s recommendation, we should use JobScheduler to wake locks to keep the phone awake while the job is running. It seems to work well and will handle the wake locks for us.
Unfortunately, this doesn’t work. Most importantly, the JobScheduler’s doze mode (if you need to send data to your server) will have a list of restrictions that will be decided by Android itself to run the jobs, and once the phone enters doze mode, the frequency of these jobs will keep increasing. Even worse, if you want to access the network (you need to send data to your server), you won’t be able to. Check the list of restrictions imposed by doze mode.
If you don’t mind not having network access and you don’t care about controlling the periodicity, JobScheduler can work just fine. In our case, we want our service to run at a very specific frequency and never stop, so we need something else.
About Foreground Services
If you have been searching the internet for a solution to this problem, you have likely ended up on this page from Android’s documentation.
There, we introduce the different types of services provided by Android. Take a look at the Foreground Service
description:
Foreground services perform operations that are noticeable to the user. For example, a music app will use a foreground service to play audio tracks. A foreground service must display a notification. Even if the user does not interact with the app, the foreground service continues to run.
This seems to be exactly what we are looking for… and it is!
My Code
Creating a foreground service
is really a simple process, so I will access and explain all the steps needed to build a never-stopping foreground service.
As usual, I have created a repository containing all the code in case you want to check it out and skip the rest of the post.
Adding Some Dependencies
In this example, I am using Kotlin coroutines with Fuel, so we will leverage coroutines and the Fuel library to handle HTTP requests.
To add these dependencies, we need to add them to our build.gradle
file:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:"+kotlin_version implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.jaredrummler:android-device-names:1.1.8' implementation 'com.github.kittinunf.fuel:fuel:2.1.0' implementation 'com.github.kittinunf.fuel:fuel-android:2.1.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' }
Our Service
Foreground Services
need to show notifications so that users know the application is still running. If you think about it, it makes sense.
Note that we have to override some callback methods that handle key aspects of the service lifecycle.
It is also very important that we use a partial wake lock to ensure our service is never affected by doze mode. Keep in mind that this will impact our phone’s battery life, so we must evaluate whether our use case can be handled by any other alternatives provided by Android to run processes in the background.
There are some utility function calls in the code (log
, setServiceState
) and some custom enums (ServiceState.STARTED
), but don’t worry too much. If you want to know their origins, check the sample repository.
class EndlessService : Service() { private var wakeLock: PowerManager.WakeLock? = null private var isServiceStarted = false override fun onBind(intent: Intent): IBinder? { log("Some component want to bind with the service") // We don't provide binding, so return null return null } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { log("onStartCommand executed with startId: $startId") if (intent != null) { val action = intent.action log("using an intent with action $action") when (action) { Actions.START.name -> startService() Actions.STOP.name -> stopService() else -> log("This should never happen. No action in the received intent") } } else { log("with a null intent. It has been probably restarted by the system.") } // by returning this we make sure the service is restarted if the system kills the service return START_STICKY } override fun onCreate() { super.onCreate() log("The service has been created".toUpperCase()) var notification = createNotification() startForeground(1, notification) } override fun onDestroy() { super.onDestroy() log("The service has been destroyed".toUpperCase()) Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show() } private fun startService() { if (isServiceStarted) return log("Starting the foreground service task") Toast.makeText(this, "Service starting its task", Toast.LENGTH_SHORT).show() isServiceStarted = true setServiceState(this, ServiceState.STARTED) // we need this lock so our service gets not affected by Doze Mode wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { acquire() } } // we're starting a loop in a coroutine GlobalScope.launch(Dispatchers.IO) { while (isServiceStarted) { launch(Dispatchers.IO) { pingFakeServer() } delay(1 * 60 * 1000) } log("End of the loop for the service") } } private fun stopService() { log("Stopping the foreground service") Toast.makeText(this, "Service stopping", Toast.LENGTH_SHORT).show() try { wakeLock?.let { if (it.isHeld) { it.release() } } stopForeground(true) stopSelf() } catch (e: Exception) { log("Service stopped without being started: ${e.message}") } isServiceStarted = false setServiceState(this, ServiceState.STOPPED) } private fun pingFakeServer() { val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmZ") val gmtTime = df.format(Date()) val deviceId = Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID) val json = """ { "deviceId": "$deviceId", "createdAt": "$gmtTime" } """ try { Fuel.post("https://jsonplaceholder.typicode.com/posts") .jsonBody(json) .response { _, _, result -> val (bytes, error) = result if (bytes != null) { log("[response bytes] ${String(bytes)}") } else { log("[response error] ${error?.message}") } } } catch (e: Exception) { log("Error making the request: ${e.message}") } } private fun createNotification(): Notification { val notificationChannelId = "ENDLESS SERVICE CHANNEL" // depending on the Android API that we're dealing with we will have to use a specific method to create the notification if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; val channel = NotificationChannel( notificationChannelId, "Endless Service notifications channel", NotificationManager.IMPORTANCE_HIGH ).let { it.description = "Endless Service channel" it.enableLights(true) it.lightColor = Color.RED it.enableVibration(true) it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400) it } notificationManager.createNotificationChannel(channel) } val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, 0) } val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder( this, notificationChannelId ) else Notification.Builder(this) return builder .setContentTitle("Endless Service") .setContentText("This is your favorite endless service working") .setContentIntent(pendingIntent) .setSmallIcon(R.mipmap.ic_launcher) .setTicker("Ticker text") .setPriority(Notification.PRIORITY_HIGH) // for under android 26 compatibility .build() } }
Time to Handle Android Manifest
We need some additional permissions FOREGROUND_SERVICE
, INTERNET
, and WAKE_LOCK
. Make sure you don’t forget to include them, or it won’t work.
Once we have them in place, we will need to declare the service.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.robertohuertas.endless"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <service android:name=".EndlessService" android:enabled="true" android:exported="false"> </service> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Starting the Service
Depending on the Android version, we must use specific methods to start the service.
If the Android version is below API 26, we must use startService, otherwise, we use startForegroundService.
Here you can see our MainActivity
, which has only one screen with two buttons to start and stop the service. This is everything you need to start our never-stopping service.
Remember, you can check the full code in this GitHub repository.
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) title = "Endless Service" findViewById<Button>(R.id.btnStartService).let { it.setOnClickListener { log("START THE FOREGROUND SERVICE ON DEMAND") actionOnService(Actions.START) } } findViewById<Button>(R.id.btnStopService).let { it.setOnClickListener { log("STOP THE FOREGROUND SERVICE ON DEMAND") actionOnService(Actions.STOP) } } } private fun actionOnService(action: Actions) { if (getServiceState(this) == ServiceState.STOPPED && action == Actions.STOP) return Intent(this, EndlessService::class.java).also { it.action = action.name if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { log("Starting the service in >=26 Mode") startForegroundService(it) return } log("Starting the service in < 26 Mode") startService(it) } } }
Effect: Start Service on Android Boot
Okay, we now have a never-stopping service that makes network requests every minute and then the user restarts the phone… our service will not restart… 🙁 (disappointed)
Don’t worry, we can find a solution for this too. We will create a BroadcastReceiver named StartReceiver
.
class StartReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_BOOT_COMPLETED && getServiceState(context) == ServiceState.STARTED) { Intent(context, EndlessService::class.java).also { it.action = Actions.START.name if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { log("Starting the service in >=26 Mode from a BroadcastReceiver") context.startForegroundService(it) return } log("Starting the service in < 26 Mode from a BroadcastReceiver") context.startService(it) } } } }
Then we will modify our Android Manifest
again and add a new permission (RECEIVE_BOOT_COMPLETED
) and our new BroadcastReceiver.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.robertohuertas.endless"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <service android:name=".EndlessService" android:enabled="true" android:exported="false"> </service> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <receiver android:enabled="true" android:name=".StartReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> </application> </manifest>
Note that the service will not restart unless it was already running. That’s just the way we programmed it, and it doesn’t have to be that way.
If you want to test this, just start an emulator with Google services and make sure to run adb in root mode.
adb root # If you get an error then you're not running the proper emulator. # Be sure to stop the service # and force a system restart: adb shell stop adb shell start # wait for the service to be restarted!
Enjoy!
Everyone is Watching
Implementing a ScrollView with Drop-down Spring Animation
Sorting Quality Projects on GitHub, Not Just Android
Custom View Imitating Alipay Sesame Credit Score Dashboard Effect
Some Thoughts on Performance Optimization of Android Apps
Welcome to Android BusBlog AreaContribute, technical growth through sharing
Looking forward to comments from friendsto discuss and learn together.