app/src/main/java/dev/equwal/inkupdate/Checker.kt (4774 bytes)
1 // SPDX-License-Identifier: GPL-3.0-or-later 2 // Copyright (c) 2026 equwal 3 package dev.equwal.inkupdate 4 5 import android.content.Context 6 import android.content.pm.PackageManager 7 8 /** 9 * The check itself: what is installed, what the two hosts say, and what that 10 * means. 11 * 12 * Every function here makes network requests. Call them from a background 13 * thread only. 14 */ 15 object Checker { 16 17 private const val GITHUB_ACCEPT = "application/vnd.github+json" 18 19 /** A watched app that is on the device. */ 20 data class Installed( 21 val watched: Watched, 22 val versionName: String, 23 val versionCode: Long, 24 val installer: String? 25 ) 26 27 /** A row of the screen: an installed app and what the last check found. */ 28 data class Row(val app: Installed, val result: Result?) 29 30 /** The watched apps that are on the device, in the order of the list. */ 31 fun installed(context: Context): List<Installed> { 32 val pm = context.packageManager 33 val out = ArrayList<Installed>() 34 for (w in Watch.ALL) { 35 val info = try { 36 pm.getPackageInfo(w.pkg, 0) 37 } catch (e: PackageManager.NameNotFoundException) { 38 null 39 } catch (e: Exception) { 40 null 41 } ?: continue 42 out.add( 43 Installed( 44 watched = w, 45 versionName = info.versionName ?: "", 46 versionCode = info.longVersionCode, 47 installer = installerOf(pm, w.pkg) 48 ) 49 ) 50 } 51 return out 52 } 53 54 /** The rows to show, from what the store holds. This makes no request. */ 55 fun rows(context: Context, store: Store): List<Row> = 56 installed(context).map { Row(it, store.result(it.watched.pkg)) } 57 58 /** 59 * Checks every installed watched app, keeps the results, and tells the 60 * person about each new update. 61 * 62 * @return how many apps have an update. 63 */ 64 fun checkAll(context: Context, store: Store): Int { 65 val apps = installed(context) 66 var updates = 0 67 var ok = true 68 for (app in apps) { 69 val r = check(app) 70 if (r is Result.Unknown) ok = false 71 store.put(app.watched.pkg, r) 72 if (r is Result.Update) { 73 updates++ 74 // The version is marked only after the notification went out. 75 // A person who turns notifications on later still hears about 76 // the update at the next check. 77 if (store.notified(app.watched.pkg) != r.versionName && 78 Notify.update(context, app.watched, r) 79 ) { 80 store.setNotified(app.watched.pkg, r.versionName) 81 } 82 } 83 } 84 store.checked(System.currentTimeMillis(), ok) 85 return updates 86 } 87 88 /** The check of one app: F-Droid, then the Play store, then GitHub. */ 89 fun check(app: Installed): Result { 90 val w = app.watched 91 val fdroid = askFdroid(w) 92 val fdroidWins = fdroid is FdroidAnswer.Found && fdroid.versionCode > app.versionCode 93 94 // Two cases make the GitHub request waste: F-Droid already has a newer 95 // build, and the Play store updates the app by itself. 96 val github = 97 if (fdroidWins || app.installer == Decide.PLAY_PACKAGE) GithubAnswer.None 98 else askGithub(w) 99 100 return Decide.source( 101 app.installer, fdroid, github, app.versionCode, app.versionName 102 ) 103 } 104 105 private fun askFdroid(w: Watched): FdroidAnswer { 106 val url = w.fdroidApi() ?: return FdroidAnswer.NotThere 107 val page = w.fdroidPage() ?: return FdroidAnswer.NotThere 108 val a = Net.get(url) 109 return when { 110 a.code == 0 -> FdroidAnswer.Failed("No answer") 111 a.code == 404 -> FdroidAnswer.NotThere 112 a.code !in 200..299 -> FdroidAnswer.Failed("F-Droid said " + a.code) 113 else -> Decide.parseFdroid(a.body, page) 114 } 115 } 116 117 private fun askGithub(w: Watched): GithubAnswer { 118 val a = Net.get(w.githubApi(), GITHUB_ACCEPT) 119 return when { 120 a.code == 0 -> GithubAnswer.Failed("No answer") 121 a.code == 403 || a.code == 429 -> GithubAnswer.Failed("Too many requests") 122 a.code !in 200..299 -> GithubAnswer.Failed("GitHub said " + a.code) 123 else -> Decide.parseGithub(a.body, w.preferAsset) 124 } 125 } 126 127 /** The package that installed the app, as the system knows it. */ 128 private fun installerOf(pm: PackageManager, pkg: String): String? = try { 129 pm.getInstallSourceInfo(pkg).installingPackageName 130 } catch (e: Exception) { 131 null 132 } 133 }