0

I'm developing a foreground service to display an overlay view. The service uses a ComposeView whose content depends on the data of a Preference DataStore.

The content of the ComposeView looks like:

// val dataStore: DataStore<Preferences> is a singleton
val settings by dataStore.data.map { /* whatever */ }.collectAsStateWithLifecycle(initialValue, context = context)

// Variable that is updated very often just to force recomposition
val time by time.collectAsStateWithLifecycle(context = context)

Text("${settings.test} $time")

Initially, everything works. But after a few minutes, the program crashes with an OutOfMemoryError. Android Studio's profiler does show a constant increase in the memory usage but does not detect a leak.

Here are some logs from Logcat that show the issue:

I  Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 45% free, 29MB/53MB, paused 372us,2.483ms total 112.599ms
I  Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 42% free, 32MB/56MB, paused 290us,1.993ms total 121.798ms
I  Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 39% free, 36MB/60MB, paused 165us,1.812ms total 118.597ms
I  Background concurrent mark compact GC freed 20MB AllocSpace bytes, 0(0B) LOS objects, 37% free, 39MB/63MB, paused 185us,2.268ms total 156.244ms

If I stop collecting settings and I only collect time, the issue disappears and the memory usage seems to be stable. Also, I was unable to reproduce the issue when the view is hosted in an Activity.

I don't know if this is a bug with Preference DataStore or if I'm doing something wrong. Here is the full code to reproduce the issue:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Since this is a test app, we just assume the overlay and notification permissions are already granted
        // Don't forget to declare the permissions and the service in the manifest
        ContextCompat.startForegroundService(this, Intent(this, MyService::class.java))
    }
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "store")

class MyService : LifecycleService(), SavedStateRegistryOwner {

    companion object {
        private const val STATUS_NOTIFICATION_ID = 1
        private const val NOTIFICATION_CHANNEL_ID = "overlay_indicator"

        private val TEST = booleanPreferencesKey("test")
    }

    private data class Settings(val test: Boolean = true)

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    private val savedStateRegistryController = SavedStateRegistryController.create(this)
    private val time = MutableStateFlow(0L)

    private lateinit var view: ComposeView

    override fun onCreate() {
        super.onCreate()
        savedStateRegistryController.performAttach()
        savedStateRegistryController.performRestore(null)

        view = ComposeView(this).apply {
            setViewTreeSavedStateRegistryOwner(this@MyService)
            setViewTreeLifecycleOwner(this@MyService)
            setContent {
                CompositionLocalProvider(LocalLifecycleOwner provides this@MyService) {
                    val context = Dispatchers.Main + job
                    val settings by dataStore.data.map { Settings(it[TEST] ?: true) }.collectAsStateWithLifecycle(Settings(), context = context)
                    val time by time.collectAsStateWithLifecycle(context = context)

                    Text("${settings.test} $time")
                }
            }
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        startForeground()

        val layoutParams = WindowManager.LayoutParams(
            WRAP_CONTENT,
            WRAP_CONTENT,
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                TYPE_APPLICATION_OVERLAY
            } else {
                @Suppress("deprecation") TYPE_SYSTEM_ALERT
            },
            FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT,
        ).apply { gravity = Gravity.BOTTOM }

        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        try {
            windowManager.addView(view, layoutParams)
        } catch (_: Exception) {

        }


        scope.launch {
            var now = TimeSource.Monotonic.markNow()
            while (true) {
                val newNow = TimeSource.Monotonic.markNow()
                time.value += (newNow - now).inWholeMicroseconds
                now = newNow
                delay(timeMillis = 1)
            }
        }

        return START_NOT_STICKY
    }

    override fun onDestroy() {
        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        windowManager.removeView(view)

        super.onDestroy()
        job.cancel()
    }

    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry


    private fun startForeground() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID, "Channel", NotificationManager.IMPORTANCE_MIN
            )
            val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID).build()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(
                STATUS_NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
            )
        } else {
            startForeground(STATUS_NOTIFICATION_ID, notification)
        }
    }
}
3
  • Have you tried mapLatest instead of map? Better yet, moving the datastore (the mapping) to a view model? Commented Apr 3 at 16:39
  • Using dataStore.data.mapLatest leads to the same result. How I am supposed to use the view model in the foreground service? I've read it is not recommended. Commented Apr 4 at 9:55
  • Update: If I create a separate settings: MutableStateFlow<Settings>, I use scope.launch { dataStore.data.collectLatest { settings.value = Settings(it[TEST] ?: true) } } and I collect newFlow instead of dataStore.data in the ComposeView content, the problem disappears. But why can't I collect the data store directly in the view? Commented Apr 4 at 11:54

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.