UpdateCheckWorker.kt

5.5 KB · raw

1package com.peachpasswords.updates23import android.app.NotificationChannel4import android.app.NotificationManager5import android.app.PendingIntent6import android.content.Context7import android.content.Intent8import android.os.Build9import androidx.core.app.NotificationCompat10import androidx.core.app.NotificationManagerCompat11import androidx.hilt.work.HiltWorker12import androidx.work.BackoffPolicy13import androidx.work.Constraints14import androidx.work.CoroutineWorker15import androidx.work.ExistingPeriodicWorkPolicy16import androidx.work.ExistingWorkPolicy17import androidx.work.NetworkType18import androidx.work.OneTimeWorkRequestBuilder19import androidx.work.PeriodicWorkRequestBuilder20import androidx.work.WorkManager21import androidx.work.WorkerParameters22import com.peachpasswords.MainActivity23import com.peachpasswords.R24import dagger.assisted.Assisted25import dagger.assisted.AssistedInject26import java.util.concurrent.TimeUnit2728/** Launch extra set by the update notification so the app opens on the update flow. */29const val EXTRA_OPEN_UPDATE = "peach_open_update"3031/**32 * The quiet half of the update flow: ask Kepr what the latest release is and, if33 * it is newer than this build, say so once in a low-importance notification.34 *35 * Kepr has no push mechanism, so this poll is the only way the app learns about a36 * release. It never downloads anything — tapping through is what starts that.37 */38@HiltWorker39class UpdateCheckWorker @AssistedInject constructor(40    @Assisted appContext: Context,41    @Assisted params: WorkerParameters,42    private val updates: UpdateRepository,43) : CoroutineWorker(appContext, params) {4445    override suspend fun doWork(): Result {46        val manifest = try {47            updates.check()48        } catch (e: Exception) {49            // Offline, DNS failure, a repo not registered yet — all transient from50            // here. Never surface a failed background check to the user.51            return Result.retry()52        } ?: return Result.success()5354        if (updates.shouldNotifyFor(manifest.version) &&55            postUpdateNotification(applicationContext, manifest.version)56        ) {57            updates.markNotified(manifest.version)58        }59        return Result.success()60    }6162    companion object {63        private const val PERIODIC_WORK = "peach_update_check"64        private const val ONE_SHOT_WORK = "peach_update_check_now"6566        private val NETWORK_ONLY = Constraints.Builder()67            .setRequiredNetworkType(NetworkType.CONNECTED)68            .build()6970        /** Once a day is plenty for a manually-installed beta; KEEP so reinstalling the schedule doesn't reset it. */71        fun schedulePeriodic(context: Context) {72            WorkManager.getInstance(context).enqueueUniquePeriodicWork(73                PERIODIC_WORK,74                ExistingPeriodicWorkPolicy.KEEP,75                PeriodicWorkRequestBuilder<UpdateCheckWorker>(1, TimeUnit.DAYS)76                    .setConstraints(NETWORK_ONLY)77                    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.MINUTES)78                    .build(),79            )80        }8182        /** The on-app-open check. REPLACE so an explicit "check now" isn't swallowed by a queued one. */83        fun checkNow(context: Context) {84            WorkManager.getInstance(context).enqueueUniqueWork(85                ONE_SHOT_WORK,86                ExistingWorkPolicy.REPLACE,87                OneTimeWorkRequestBuilder<UpdateCheckWorker>()88                    .setConstraints(NETWORK_ONLY)89                    .build(),90            )91        }92    }93}9495private const val CHANNEL_ID = "peach_updates"96private const val NOTIFICATION_ID = 43019798/**99 * Post the "an update exists" notification. Returns false when the user has not100 * granted POST_NOTIFICATIONS (API 33+) or has muted the channel — the check still101 * counts as done, but it must not be recorded as notified, or the grant arriving102 * later would never produce a notification for this version.103 */104fun postUpdateNotification(context: Context, version: String): Boolean {105    val manager = NotificationManagerCompat.from(context)106    if (!manager.areNotificationsEnabled()) return false107108    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {109        val system = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager110        system.createNotificationChannel(111            NotificationChannel(CHANNEL_ID, "Peach updates", NotificationManager.IMPORTANCE_LOW).apply {112                description = "Tells you when a newer version of Peach is available."113                setShowBadge(false)114            },115        )116    }117118    val open = PendingIntent.getActivity(119        context,120        0,121        Intent(context, MainActivity::class.java)122            .putExtra(EXTRA_OPEN_UPDATE, true)123            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP),124        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,125    )126127    val notification = NotificationCompat.Builder(context, CHANNEL_ID)128        .setContentTitle("Peach update available — v$version")129        .setContentText("Tap to see what changed and install it.")130        .setSmallIcon(R.drawable.ic_launcher_monochrome)131        .setPriority(NotificationCompat.PRIORITY_LOW)132        .setAutoCancel(true)133        .setContentIntent(open)134        .build()135136    return try {137        manager.notify(NOTIFICATION_ID, notification)138        true139    } catch (e: SecurityException) {140        // areNotificationsEnabled() and the actual permission can disagree on some141        // OEM builds; a missing notification must never take the app down.142        false143    }144}