I want to implement just a simple timer which runs in background. For this purpose I have wrote it as a foreground service special use (its use does not fit the existent foreground services).
I decided to use a Handler running in a HandlerThread as I do not want to run any task in the main loop. Notifications, start/stop commands (see TimerService code below)
The service is started by the view model which collects the elapsed time from the timer service (see TimerViewModel code below).
In MainActivity self permissions are checked.
Before starting the app I set it manually as "unrestricted" (Allow background usage).
It works properly (elapsed time is updated in foreground notification) as long as the phone is:
When I am NOT debugging/connected/charging and in this state I leave the App in background (display off) for about 180 seconds, then when I check the notification message I see the elapsed time begins to be delayed (e.g. 160 secs) ... the more seconds the more I leave the phone in background.
My Pixel 8 Pro has Android 16, App compileSdk/targetSdk = 36.
I would appreciate it if anyone here could shed a little light on this issue.
thanks in advance!
TimerService
class TimerService : Service() {
private var builder: NotificationCompat.Builder? = null
private lateinit var serviceHandler: Handler
private lateinit var handlerThread: HandlerThread
private val timerRunnable: Runnable = object : Runnable {
override fun run() {
serviceHandler.postDelayed(this, ONE_SECOND_MILLIS)
_timerStateFlow.value++
updateNotification(_timerStateFlow.value)
}
}
override fun onCreate() {
super.onCreate()
// Create a new background thread for the handler
handlerThread = HandlerThread("TimerServiceHandlerThread").apply {
start()
}
serviceHandler = Handler(handlerThread.looper)
createNotificationChannel()
_timerStateFlow.value = 0
_isTimerRunning.value = false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> startTimerService()
ACTION_STOP -> stopTimerService()
}
return START_STICKY
}
private fun startTimerService() {
val notification = createNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(NOTIFICATION_ID, notification)
}
if (!_isTimerRunning.value) {
_timerStateFlow.value = 0
}
startTimer()
}
private fun startTimer() {
if (!_isTimerRunning.value) {
_isTimerRunning.value = true
// Start the timer on the background thread.
// The first tick will be after 1000ms.
serviceHandler.postDelayed(timerRunnable, ONE_SECOND_MILLIS)
}
}
private fun stopTimer() {
// Remove callbacks from the background thread handler.
if (::serviceHandler.isInitialized) {
serviceHandler.removeCallbacks(timerRunnable)
}
_isTimerRunning.value = false
}
private fun stopTimerService() {
stopTimer()
_timerStateFlow.value = 0
stopSelf()
}
private fun createNotification(): Notification {
builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Timer Service Active")
.setSmallIcon(R.drawable.outline_timer_24)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
return builder!!.build()
}
private fun updateNotification(count: Int) {
builder?.let {
val hh = count / 3600
val mm = (count % 3600) / 60
val ss = count % 60
val timeString = String.format("%02d:%02d:%02d", hh, mm, ss)
it.setContentText("timer is running $timeString")
val manager = getSystemService(NotificationManager::class.java)
manager.notify(NOTIFICATION_ID, it.build())
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Timer Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}
override fun onDestroy() {
stopTimer()
// Quit the background thread.
if (::handlerThread.isInitialized) {
handlerThread.quitSafely()
}
stopForeground(STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
// We don't provide binding, so return null
return null
}
companion object {
const val ACTION_START = "com.example.app.START"
const val ACTION_STOP = "com.example.app.STOP"
const val ACTION_PAUSE = "com.example.app.PAUSE"
const val CHANNEL_ID = "TimerServiceChannel"
const val NOTIFICATION_ID = 1
const val ONE_SECOND_MILLIS = 1000L
private val _timerStateFlow = MutableStateFlow(0)
val timerStateFlow = _timerStateFlow.asStateFlow()
private val _isTimerRunning = MutableStateFlow(false)
val isTimerRunning = _isTimerRunning.asStateFlow()
}
}
TimerViewModel
class TimerViewModel(...) : AndroidViewModel(application) {
private val _state = MutableStateFlow(WorkoutPlayState())
val state by lazy { _state.asStateFlow() }
// Collect the timer value from the service
val elapsedTime: StateFlow<Int> = TimerService.timerStateFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = TimerService.timerStateFlow.value // Get current value on init
)
init {
viewModelScope.launch {
launch {
elapsedTime
.collect { newTime ->
Timber.d("Timer tick: $newTime")
}
}
}
}
...
private fun startTimerService() {
val intent = Intent(application.applicationContext, TimerService::class.java).apply {
action = TimerService.ACTION_START
}
startForegroundService(application.applicationContext, intent)
}
}
Manifest
<service
android:name=".service.TimerService"
android:foregroundServiceType="specialUse"
android:exported="false">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="App background timer"/>
</service>
Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionsToRequest = arrayOf(
Manifest.permission.POST_NOTIFICATIONS
)
if (!hasPermissions(this, *permissionsToRequest)) {
ActivityCompat.requestPermissions(this, permissionsToRequest, 1)
}
}
...
}
...
}
I supposed that if I start a foreground service special use this would automatically make my app to keep running in background but this was wrong.
As shown in Choose the right technology the path led me to "Mannually set a wake lock" as I am not using an API that keeps the device awake.
I tested my app with Pixel 8 Pro A16, Samsung J3 A9. Also Huawei but with App Launch no battery optimization (set manually).
class TimerService : Service() {
private val wakeLock: PowerManager.WakeLock by lazy {
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "TimerService::lock")
}
...
override fun onCreate() {
super.onCreate()
wakeLock.acquire() <<<<-------
createNotificationChannel()
initTimer()
}
...
private fun stopTimer() {
// Remove callbacks from the background thread handler.
if (::serviceHandler.isInitialized) {
serviceHandler.removeCallbacks(timerRunnable)
}
_isTimerRunning.value = false
_timerStateFlow.value = 0
if (wakeLock.isHeld) { <<<<------
wakeLock.release()
}
}
...
}
Interesting links I found
Hope this helps other devs with the same issue.
Best regards!