mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
updates: beta channel, and a switch to turn off automatic checks
Beta updates counts GitHub pre-releases as updates. With Check automatically off, Gearslip contacts GitHub only when you tap Check for updates.
This commit is contained in:
1 parent
fb6bc018d5
commit
fff2db63b0
3 files changed
+90
-32
No files matched your search
@@ -137,7 +137,9 @@ private fun AboutSection(onOpenWhatsNew: () -> Unit, onReplayTutorial: () -> Uni
|
|||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
UpdateRow()
|
UpdateRow()
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
UpdateNotifyRow()
|
UpdateAutoRow()
|
||||||
|
SettingsDivider()
|
||||||
|
UpdateBetaRow()
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
"Replay the setup guide",
|
"Replay the setup guide",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.selection.toggleable
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -130,19 +131,38 @@ internal fun UpdateRow() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether a drive's end may notify about a new version. */
|
/** Whether Gearslip checks for updates on its own. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun UpdateNotifyRow() {
|
internal fun UpdateAutoRow() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var on by remember { mutableStateOf(UpdateChecker.notifyEnabled(context)) }
|
var on by remember { mutableStateOf(UpdateChecker.autoCheckEnabled(context)) }
|
||||||
SettingsRow(
|
SettingsRow(
|
||||||
"Update notifications",
|
"Check automatically",
|
||||||
Modifier.toggleable(on, role = Role.Switch) {
|
Modifier.toggleable(on, role = Role.Switch) {
|
||||||
on = it
|
on = it
|
||||||
UpdateChecker.setNotifyEnabled(context, it)
|
UpdateChecker.setAutoCheckEnabled(context, it)
|
||||||
},
|
},
|
||||||
subtitle = "On the car screen when you plug in, and on the phone after a drive",
|
subtitle = if (BuildConfig.DEBUG) "Does nothing in dev builds. They check only when you tap Check for updates"
|
||||||
|
else if (on) "Once a day, with a note on the car screen and the phone when one is out"
|
||||||
|
else "Gearslip checks only when you tap Check for updates",
|
||||||
icon = Icons.Filled.Notifications,
|
icon = Icons.Filled.Notifications,
|
||||||
trailing = { Switch(checked = on, onCheckedChange = null) },
|
trailing = { Switch(checked = on, onCheckedChange = null) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether pre-releases count as updates. */
|
||||||
|
@Composable
|
||||||
|
internal fun UpdateBetaRow() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var on by remember { mutableStateOf(UpdateChecker.betaEnabled(context)) }
|
||||||
|
SettingsRow(
|
||||||
|
"Beta updates",
|
||||||
|
Modifier.toggleable(on, role = Role.Switch) {
|
||||||
|
on = it
|
||||||
|
UpdateChecker.setBetaEnabled(context, it)
|
||||||
|
},
|
||||||
|
subtitle = "Get test versions before they're released. They may have more bugs.",
|
||||||
|
icon = Icons.Filled.Build,
|
||||||
|
trailing = { Switch(checked = on, onCheckedChange = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -32,8 +32,10 @@ import java.security.MessageDigest
|
|||||||
/**
|
/**
|
||||||
* Looks for a newer Gearslip on GitHub and installs it through Android's own installer.
|
* Looks for a newer Gearslip on GitHub and installs it through Android's own installer.
|
||||||
*
|
*
|
||||||
* Only GitHub's "latest" release counts, which leaves out pre-releases and drafts. The app checks
|
* Only GitHub's "latest" release counts, which leaves out pre-releases and drafts, unless the
|
||||||
* at most once a day: when it opens, and when a drive ends. After a drive a new version also gets
|
* driver opted into betas: then pre-releases count too. The app checks at most once a day: when
|
||||||
|
* it opens, and when a drive ends. With automatic checks off it never asks GitHub on its own,
|
||||||
|
* only when the driver taps Check for updates. After a drive a new version also gets
|
||||||
* a notification, once per version, since the driver is parked and the app is likely closed. The
|
* a notification, once per version, since the driver is parked and the app is likely closed. The
|
||||||
* car screen shows the same news once per version, at plug-in and only while parked.
|
* car screen shows the same news once per version, at plug-in and only while parked.
|
||||||
* Nothing runs in the background otherwise, and nothing else is sent: the request is the same one
|
* Nothing runs in the background otherwise, and nothing else is sent: the request is the same one
|
||||||
@@ -62,7 +64,9 @@ object UpdateChecker {
|
|||||||
private const val PREFS = "updates"
|
private const val PREFS = "updates"
|
||||||
private const val KEY_CHECKED_AT = "checked_at"
|
private const val KEY_CHECKED_AT = "checked_at"
|
||||||
private const val KEY_FOUND = "found"
|
private const val KEY_FOUND = "found"
|
||||||
private const val KEY_NOTIFY = "notify"
|
/** Named for the notifications toggle it grew out of; it now covers every automatic check. */
|
||||||
|
private const val KEY_AUTO = "notify"
|
||||||
|
private const val KEY_BETA = "beta"
|
||||||
private const val KEY_NOTIFIED = "notified_version"
|
private const val KEY_NOTIFIED = "notified_version"
|
||||||
private const val KEY_CAR_NOTIFIED = "car_notified_version"
|
private const val KEY_CAR_NOTIFIED = "car_notified_version"
|
||||||
/** Lets the car screen settle after plug-in, so the card isn't lost behind the first frames. */
|
/** Lets the car screen settle after plug-in, so the card isn't lost behind the first frames. */
|
||||||
@@ -71,22 +75,43 @@ object UpdateChecker {
|
|||||||
private const val NOTIFICATION_ID = 4100
|
private const val NOTIFICATION_ID = 4100
|
||||||
private const val DAY_MS = 24 * 60 * 60 * 1000L
|
private const val DAY_MS = 24 * 60 * 60 * 1000L
|
||||||
private const val LATEST = "https://api.github.com/repos/Seb3thehacker/gearslip/releases/latest"
|
private const val LATEST = "https://api.github.com/repos/Seb3thehacker/gearslip/releases/latest"
|
||||||
|
/** Newest first, pre-releases included. Ten covers any run of betas between two releases. */
|
||||||
|
private const val ALL = "https://api.github.com/repos/Seb3thehacker/gearslip/releases?per_page=10"
|
||||||
|
|
||||||
/** Set when the driver tapped Install but first had to allow installs from Gearslip. */
|
/** Set when the driver tapped Install but first had to allow installs from Gearslip. */
|
||||||
@Volatile private var installWhenAllowed = false
|
@Volatile private var installWhenAllowed = false
|
||||||
|
|
||||||
private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
/** Whether a drive's end may post a notification about a new version. On by default: it sends nothing. */
|
/**
|
||||||
fun notifyEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_NOTIFY, true)
|
* Whether Gearslip checks on its own, and tells the driver what it finds on the phone and the
|
||||||
|
* car screen. On by default: the check sends nothing about the phone.
|
||||||
|
*/
|
||||||
|
fun autoCheckEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_AUTO, true)
|
||||||
|
|
||||||
fun setNotifyEnabled(context: Context, on: Boolean) {
|
fun setAutoCheckEnabled(context: Context, on: Boolean) {
|
||||||
prefs(context).edit().putBoolean(KEY_NOTIFY, on).apply()
|
prefs(context).edit().putBoolean(KEY_AUTO, on).apply()
|
||||||
if (!on) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
if (!on) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether GitHub pre-releases count as updates. */
|
||||||
|
fun betaEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_BETA, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switching channels forgets what the old one found: a beta found earlier shouldn't still be
|
||||||
|
* offered once betas are off. Checks again right away if automatic checks are on.
|
||||||
|
*/
|
||||||
|
fun setBetaEnabled(context: Context, on: Boolean) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_BETA, on).remove(KEY_FOUND).remove(KEY_CHECKED_AT).apply()
|
||||||
|
val current = flow.value
|
||||||
|
if (current is State.Checking || current is State.Downloading) return
|
||||||
|
updatesDir(context).deleteRecursively()
|
||||||
|
flow.value = State.Idle
|
||||||
|
if (autoCheckEnabled(context) && !BuildConfig.DEBUG) check(context)
|
||||||
|
}
|
||||||
|
|
||||||
private fun dueForCheck(context: Context) =
|
private fun dueForCheck(context: Context) =
|
||||||
!BuildConfig.DEBUG && System.currentTimeMillis() - prefs(context).getLong(KEY_CHECKED_AT, 0) >= DAY_MS
|
!BuildConfig.DEBUG && autoCheckEnabled(context) && System.currentTimeMillis() - prefs(context).getLong(KEY_CHECKED_AT, 0) >= DAY_MS
|
||||||
|
|
||||||
/** Gearslip came to the front: check if a day has passed, and finish an install that waited on permission. */
|
/** Gearslip came to the front: check if a day has passed, and finish an install that waited on permission. */
|
||||||
fun onAppOpened(context: Context) {
|
fun onAppOpened(context: Context) {
|
||||||
@@ -107,7 +132,7 @@ object UpdateChecker {
|
|||||||
* notification for a new version they haven't been told about yet.
|
* notification for a new version they haven't been told about yet.
|
||||||
*/
|
*/
|
||||||
fun onDriveEnded(context: Context) {
|
fun onDriveEnded(context: Context) {
|
||||||
if (!notifyEnabled(context)) return
|
if (!autoCheckEnabled(context)) return
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
Thread({
|
Thread({
|
||||||
val current = flow.value
|
val current = flow.value
|
||||||
@@ -126,7 +151,7 @@ object UpdateChecker {
|
|||||||
* once per version. Skipped while moving; the next plug-in tries again.
|
* once per version. Skipped while moving; the next plug-in tries again.
|
||||||
*/
|
*/
|
||||||
fun onCarConnected(context: Context) {
|
fun onCarConnected(context: Context) {
|
||||||
if (!notifyEnabled(context)) return
|
if (!autoCheckEnabled(context)) return
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
Thread({
|
Thread({
|
||||||
val current = flow.value
|
val current = flow.value
|
||||||
@@ -197,7 +222,7 @@ object UpdateChecker {
|
|||||||
Thread({ flow.value = checkNow(app) }, "gearslip-update").start()
|
Thread({ flow.value = checkNow(app) }, "gearslip-update").start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkNow(app: Context): State = runCatching { fetchLatest() }.fold(
|
private fun checkNow(app: Context): State = runCatching { fetchNewest(betaEnabled(app)) }.fold(
|
||||||
onSuccess = { release ->
|
onSuccess = { release ->
|
||||||
prefs(app).edit().putLong(KEY_CHECKED_AT, System.currentTimeMillis()).apply()
|
prefs(app).edit().putLong(KEY_CHECKED_AT, System.currentTimeMillis()).apply()
|
||||||
if (release != null && isNewer(release.version, BuildConfig.VERSION_NAME)) {
|
if (release != null && isNewer(release.version, BuildConfig.VERSION_NAME)) {
|
||||||
@@ -263,8 +288,9 @@ object UpdateChecker {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchLatest(): Release? {
|
/** The newest release with an APK: the "latest" one, or with [beta] the newest of any kind but drafts. */
|
||||||
val conn = (URL(LATEST).openConnection() as HttpURLConnection).apply {
|
private fun fetchNewest(beta: Boolean): Release? {
|
||||||
|
val conn = (URL(if (beta) ALL else LATEST).openConnection() as HttpURLConnection).apply {
|
||||||
connectTimeout = 15_000
|
connectTimeout = 15_000
|
||||||
readTimeout = 15_000
|
readTimeout = 15_000
|
||||||
setRequestProperty("Accept", "application/vnd.github+json")
|
setRequestProperty("Accept", "application/vnd.github+json")
|
||||||
@@ -273,23 +299,33 @@ object UpdateChecker {
|
|||||||
try {
|
try {
|
||||||
if (conn.responseCode == 404) return null // no stable release yet
|
if (conn.responseCode == 404) return null // no stable release yet
|
||||||
if (conn.responseCode !in 200..299) error("GitHub answered ${conn.responseCode}")
|
if (conn.responseCode !in 200..299) error("GitHub answered ${conn.responseCode}")
|
||||||
val json = JSONObject(conn.inputStream.bufferedReader().use { it.readText() })
|
val body = conn.inputStream.bufferedReader().use { it.readText() }
|
||||||
val assets = json.optJSONArray("assets") ?: return null
|
if (!beta) return release(JSONObject(body))
|
||||||
val apk = (0 until assets.length()).map { assets.getJSONObject(it) }
|
val list = org.json.JSONArray(body)
|
||||||
.firstOrNull { it.optString("name").endsWith(".apk") && !it.optString("name").contains("debug") }
|
return (0 until list.length()).map { list.getJSONObject(it) }
|
||||||
?: return null
|
.filterNot { it.optBoolean("draft") }
|
||||||
return Release(
|
.mapNotNull(::release)
|
||||||
version = json.getString("tag_name").removePrefix("v"),
|
.reduceOrNull { best, next -> if (isNewer(next.version, best.version)) next else best }
|
||||||
apkUrl = apk.getString("browser_download_url"),
|
|
||||||
size = apk.optLong("size", -1),
|
|
||||||
sha256 = apk.optString("digest").takeIf { it.startsWith("sha256:") }?.removePrefix("sha256:"),
|
|
||||||
pageUrl = json.optString("html_url"),
|
|
||||||
)
|
|
||||||
} finally {
|
} finally {
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A release as GitHub lists it, or null when it has no APK attached. */
|
||||||
|
private fun release(json: JSONObject): Release? {
|
||||||
|
val assets = json.optJSONArray("assets") ?: return null
|
||||||
|
val apk = (0 until assets.length()).map { assets.getJSONObject(it) }
|
||||||
|
.firstOrNull { it.optString("name").endsWith(".apk") && !it.optString("name").contains("debug") }
|
||||||
|
?: return null
|
||||||
|
return Release(
|
||||||
|
version = json.getString("tag_name").removePrefix("v"),
|
||||||
|
apkUrl = apk.getString("browser_download_url"),
|
||||||
|
size = apk.optLong("size", -1),
|
||||||
|
sha256 = apk.optString("digest").takeIf { it.startsWith("sha256:") }?.removePrefix("sha256:"),
|
||||||
|
pageUrl = json.optString("html_url"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchApk(release: Release, into: File) {
|
private fun fetchApk(release: Release, into: File) {
|
||||||
val conn = (URL(release.apkUrl).openConnection() as HttpURLConnection).apply {
|
val conn = (URL(release.apkUrl).openConnection() as HttpURLConnection).apply {
|
||||||
connectTimeout = 15_000
|
connectTimeout = 15_000
|
||||||
|
|||||||
Reference in new issue
Block a user