Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ dependencies {
testImplementation libs.mockk
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.androidx.room.testing
testImplementation libs.androidx.work.testing

//
// Runtime dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,22 @@ class CleanupWidgetsWorker @AssistedInject constructor(
@JvmField
val TAG: String = CleanupWidgetsWorker::class.java.simpleName

fun enableWorker(application: Application) {
val workManager = WorkManager.getInstance(application)
/** Unique name of a one-off cleanup; separate from [TAG] so the periodic work can't cancel it. */
const val CLEANUP_NOW = "CleanupWidgetsWorker.now"

workManager.enqueueUniqueWork(
TAG,
/** Removes the records of widgets that are no longer on the home screen, as soon as possible. */
fun runOnce(context: Context) {
WorkManager.getInstance(context).enqueueUniqueWork(
CLEANUP_NOW,
ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequestBuilder<CleanupWidgetsWorker>().build()
)
}

fun enableWorker(application: Application) {
val workManager = WorkManager.getInstance(application)

runOnce(application)
workManager.enqueueUniquePeriodicWork(
TAG,
ExistingPeriodicWorkPolicy.REPLACE,
Expand Down
26 changes: 6 additions & 20 deletions app/src/main/java/de/psdev/devdrawer/appwidget/DDWidgetProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,25 @@ package de.psdev.devdrawer.appwidget
import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import dagger.hilt.android.AndroidEntryPoint
import de.psdev.devdrawer.appwidget.glance.DevDrawerGlanceWidget
import de.psdev.devdrawer.database.DevDrawerDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import mu.KLogging
import javax.inject.Inject

/**
* NOTE: Never rename this as it will break existing widgets.
*/
@AndroidEntryPoint
class DDWidgetProvider : GlanceAppWidgetReceiver() {

@Inject
lateinit var devDrawerDatabase: DevDrawerDatabase

companion object : KLogging()

override val glanceAppWidget: GlanceAppWidget = DevDrawerGlanceWidget()

/**
* Glance's own [onDeleted] already takes the broadcast's goAsync() to clear the widget's state; a second
* goAsync() returns null. The database records are removed by a worker instead.
*/
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
logger.warn { "Deleted widgets ${appWidgetIds.joinToString()}" }
val pendingResult = goAsync()
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
try {
devDrawerDatabase.widgetDao().deleteByIds(appWidgetIds.toList())
} finally {
pendingResult.finish()
}
}
logger.info { "Deleted widgets ${appWidgetIds.joinToString()}" }
CleanupWidgetsWorker.runOnce(context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package de.psdev.devdrawer.appwidget

import android.app.Application
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import androidx.work.Configuration
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
// SDK 36 requires Java 21; use SDK 33 which is compatible with the project's Java 17 toolchain.
@Config(sdk = [33])
class WidgetDeletionTest {

private val application: Application = ApplicationProvider.getApplicationContext()
private val workManager get() = WorkManager.getInstance(application)

@Before
fun setUp() {
// Stands in for the Hilt worker factory: every worker just succeeds, so its state shows it ran.
val factory = object : WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters) =
object : Worker(appContext, workerParameters) {
override fun doWork() = Result.success()
}
}
WorkManagerTestInitHelper.initializeTestWorkManager(
application,
Configuration.Builder().setWorkerFactory(factory).setExecutor(SynchronousExecutor()).build()
)
}

private fun cleanupStates() =
workManager.getWorkInfosForUniqueWork(CleanupWidgetsWorker.CLEANUP_NOW).get().map { it.state }

@Test
fun `given a widget removed from the home screen, when the provider is told, then it does not crash and runs a cleanup`() {
// Given
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED)
.setComponent(ComponentName(application, DDWidgetProvider::class.java))
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 7)

// When
application.sendBroadcast(intent)
shadowOf(application.mainLooper).idle()

// Then
assertEquals(listOf(WorkInfo.State.SUCCEEDED), cleanupStates())
}

@Test
fun `given the app starts, when the cleanup workers are enabled, then the one-off cleanup runs and is not cancelled by the periodic one`() {
// Given / When
CleanupWidgetsWorker.enableWorker(application)

// Then
assertEquals(listOf(WorkInfo.State.SUCCEEDED), cleanupStates())
assertEquals(
listOf(WorkInfo.State.ENQUEUED),
workManager.getWorkInfosForUniqueWork(CleanupWidgetsWorker.TAG).get().map { it.state }
)
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview
androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "playUpdate" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3" }
Expand Down
Loading