mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
Stable updates install by themselves on Wi-Fi when no car is connected
This commit is contained in:
1 parent
9d6d18bc47
commit
31d8232978
5 files changed
+236
-13
No files matched your search
@@ -5,6 +5,8 @@
|
|||||||
<!-- The projected WebView loads real pages; without this every load fails silently. -->
|
<!-- The projected WebView loads real pages; without this every load fails silently. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<!-- Lets an update Gearslip installed itself skip Android's prompt. -->
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Phone mirroring. Android refuses to hand over a MediaProjection unless a foreground
|
Phone mirroring. Android refuses to hand over a MediaProjection unless a foreground
|
||||||
@@ -216,6 +218,14 @@
|
|||||||
android:name=".notify.TestMessageReceiver"
|
android:name=".notify.TestMessageReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".update.UpdateReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".mirror.MirrorService"
|
android:name=".mirror.MirrorService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ private fun UpdateDialog(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var auto by remember { mutableStateOf(UpdateChecker.autoCheckEnabled(context)) }
|
var auto by remember { mutableStateOf(UpdateChecker.autoCheckEnabled(context)) }
|
||||||
|
var silent by remember { mutableStateOf(UpdateChecker.silentEnabled(context)) }
|
||||||
val actionLabels = listOf(R.string.update_download, R.string.update_install, R.string.update_try_again, R.string.update_check_now).map { stringResource(it) }
|
val actionLabels = listOf(R.string.update_download, R.string.update_install, R.string.update_try_again, R.string.update_check_now).map { stringResource(it) }
|
||||||
val action: Pair<String, () -> Unit>? = when (state) {
|
val action: Pair<String, () -> Unit>? = when (state) {
|
||||||
State.Checking, is State.Downloading -> null
|
State.Checking, is State.Downloading -> null
|
||||||
@@ -230,6 +231,29 @@ private fun UpdateDialog(
|
|||||||
}
|
}
|
||||||
Switch(checked = auto, onCheckedChange = null)
|
Switch(checked = auto, onCheckedChange = null)
|
||||||
}
|
}
|
||||||
|
// Silent installs ride on the automatic check, so the switch only shows with it on.
|
||||||
|
if (auto && !BuildConfig.DEBUG) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(silent, role = Role.Switch) {
|
||||||
|
silent = it
|
||||||
|
UpdateChecker.setSilentEnabled(context, it)
|
||||||
|
}
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(stringResource(R.string.update_silent), style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.update_silent_sub),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(checked = silent, onCheckedChange = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
package app.seb3thehacker.gearslip.update
|
package app.seb3thehacker.gearslip.update
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import app.seb3thehacker.gearslip.BuildConfig
|
import app.seb3thehacker.gearslip.BuildConfig
|
||||||
import app.seb3thehacker.gearslip.GearslipActivity
|
import app.seb3thehacker.gearslip.GearslipActivity
|
||||||
import app.seb3thehacker.gearslip.GearslipLog
|
import app.seb3thehacker.gearslip.GearslipLog
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.SessionStatus
|
import app.seb3thehacker.gearslip.SessionStatus
|
||||||
import app.seb3thehacker.gearslip.notify.CarMotion
|
import app.seb3thehacker.gearslip.notify.CarMotion
|
||||||
import app.seb3thehacker.gearslip.notify.CarNotification
|
import app.seb3thehacker.gearslip.notify.CarNotification
|
||||||
import app.seb3thehacker.gearslip.notify.CarNotifications
|
import app.seb3thehacker.gearslip.notify.CarNotifications
|
||||||
|
import app.seb3thehacker.gearslip.wireless.WirelessAuto
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -42,6 +48,10 @@ import java.security.MessageDigest
|
|||||||
* 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
|
||||||
* a browser makes for the release page. Debug builds never check on their own.
|
* a browser makes for the release page. Debug builds never check on their own.
|
||||||
*
|
*
|
||||||
|
* On Stable, a new version also installs by itself on Wi-Fi while no car is connected, without
|
||||||
|
* Android asking, once Gearslip has installed itself before. Android allows that only to the app
|
||||||
|
* that installed the current version, so the first update from a browser download still asks.
|
||||||
|
*
|
||||||
* The APK is checked against the size and SHA-256 GitHub lists for it, and Android refuses an
|
* The APK is checked against the size and SHA-256 GitHub lists for it, and Android refuses an
|
||||||
* update signed with a different key, so a bad download can't replace the app.
|
* update signed with a different key, so a bad download can't replace the app.
|
||||||
*/
|
*/
|
||||||
@@ -72,6 +82,12 @@ object UpdateChecker {
|
|||||||
private const val KEY_CHANNEL = "channel"
|
private const val KEY_CHANNEL = "channel"
|
||||||
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"
|
||||||
|
private const val KEY_SILENT = "silent"
|
||||||
|
/** The version installing without a prompt, so the new one can say it updated. */
|
||||||
|
private const val KEY_SILENT_VERSION = "silent_version"
|
||||||
|
/** A version Android wanted confirmed: it isn't tried quietly again, the card and notification take over. */
|
||||||
|
private const val KEY_SILENT_REFUSED = "silent_refused"
|
||||||
|
private const val UPDATED_NOTIFICATION_ID = 4101
|
||||||
/** 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. */
|
||||||
private const val CAR_DELAY_MS = 8_000L
|
private const val CAR_DELAY_MS = 8_000L
|
||||||
private const val CHANNEL = "updates"
|
private const val CHANNEL = "updates"
|
||||||
@@ -97,6 +113,13 @@ object UpdateChecker {
|
|||||||
if (!on) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
if (!on) NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether a Stable update installs by itself on Wi-Fi with no car connected. On by default. */
|
||||||
|
fun silentEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_SILENT, true)
|
||||||
|
|
||||||
|
fun setSilentEnabled(context: Context, on: Boolean) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_SILENT, on).apply()
|
||||||
|
}
|
||||||
|
|
||||||
/** Which releases count as updates. Each channel also takes everything the one before it does. */
|
/** Which releases count as updates. Each channel also takes everything the one before it does. */
|
||||||
enum class Channel { STABLE, BETA, ALPHA }
|
enum class Channel { STABLE, BETA, ALPHA }
|
||||||
|
|
||||||
@@ -130,10 +153,27 @@ object UpdateChecker {
|
|||||||
install(context)
|
install(context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Opened for a car (plugged in, or wireless found it): installing now would close Gearslip
|
||||||
|
// just as it connects.
|
||||||
|
val forCar = (context as? Activity)?.intent?.action in
|
||||||
|
setOf(UsbManager.ACTION_USB_ACCESSORY_ATTACHED, WirelessAuto.ACTION_CAR_NEARBY)
|
||||||
|
if (current is State.Ready) {
|
||||||
|
if (!forCar) installQuietly(context.applicationContext, current.release)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (current != State.Idle) return
|
if (current != State.Idle) return
|
||||||
// A release found earlier still shows after Android closed the app.
|
// A release found earlier still shows after Android closed the app.
|
||||||
savedRelease(context)?.let { flow.value = State.Available(it) }
|
savedRelease(context)?.let { flow.value = State.Available(it) }
|
||||||
if (dueForCheck(context)) check(context)
|
val app = context.applicationContext
|
||||||
|
if (dueForCheck(context)) {
|
||||||
|
flow.value = State.Checking
|
||||||
|
Thread({
|
||||||
|
flow.value = checkNow(app)
|
||||||
|
(flow.value as? State.Available)?.let { if (!forCar) installQuietly(app, it.release) }
|
||||||
|
}, "gearslip-update").start()
|
||||||
|
} else if (!forCar) {
|
||||||
|
savedRelease(app)?.let { installQuietly(app, it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,6 +190,7 @@ object UpdateChecker {
|
|||||||
flow.value = checkNow(app)
|
flow.value = checkNow(app)
|
||||||
}
|
}
|
||||||
val release = foundRelease(app) ?: return@Thread
|
val release = foundRelease(app) ?: return@Thread
|
||||||
|
if (installQuietly(app, release)) return@Thread
|
||||||
if (prefs(app).getString(KEY_NOTIFIED, null) == release.version) return@Thread
|
if (prefs(app).getString(KEY_NOTIFIED, null) == release.version) return@Thread
|
||||||
if (notify(app, release)) prefs(app).edit().putString(KEY_NOTIFIED, release.version).apply()
|
if (notify(app, release)) prefs(app).edit().putString(KEY_NOTIFIED, release.version).apply()
|
||||||
}, "gearslip-update").start()
|
}, "gearslip-update").start()
|
||||||
@@ -250,8 +291,34 @@ object UpdateChecker {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Downloads [release], then opens the installer once the file checks out. */
|
/**
|
||||||
fun download(context: Context, release: Release) {
|
* Downloads and installs [release] without asking, when everything allows it: Stable, the
|
||||||
|
* switch on, Wi-Fi, no car, and Android already letting Gearslip install. Returns false when it
|
||||||
|
* didn't start, so the caller tells the driver the usual way.
|
||||||
|
*/
|
||||||
|
private fun installQuietly(app: Context, release: Release): Boolean {
|
||||||
|
val ok = !BuildConfig.DEBUG && autoCheckEnabled(app) && silentEnabled(app) &&
|
||||||
|
channel(app) == Channel.STABLE && !carConnected && onWifi(app) &&
|
||||||
|
app.packageManager.canRequestPackageInstalls() &&
|
||||||
|
prefs(app).getString(KEY_SILENT_REFUSED, null) != release.version
|
||||||
|
if (!ok) return false
|
||||||
|
GearslipLog.i("update: installing ${release.version} without asking")
|
||||||
|
val current = flow.value
|
||||||
|
if (current is State.Ready && current.release == release) install(app, quiet = true)
|
||||||
|
else download(app, release, quiet = true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Not metered: Wi-Fi or ethernet, where 34 MB doesn't cost anyone. */
|
||||||
|
private fun onWifi(context: Context): Boolean {
|
||||||
|
val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false
|
||||||
|
val caps = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
||||||
|
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
|
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downloads [release], then installs it once the file checks out. */
|
||||||
|
fun download(context: Context, release: Release, quiet: Boolean = false) {
|
||||||
if (flow.value is State.Downloading) return
|
if (flow.value is State.Downloading) return
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
flow.value = State.Downloading(release, 0f)
|
flow.value = State.Downloading(release, 0f)
|
||||||
@@ -267,21 +334,23 @@ object UpdateChecker {
|
|||||||
GearslipLog.w("update: download failed: ${it.message}")
|
GearslipLog.w("update: download failed: ${it.message}")
|
||||||
State.Failed(it.message ?: "The download failed. Try again.", release)
|
State.Failed(it.message ?: "The download failed. Try again.", release)
|
||||||
}
|
}
|
||||||
if (flow.value is State.Ready) install(app)
|
if (flow.value is State.Ready) install(app, quiet)
|
||||||
}, "gearslip-update").start()
|
}, "gearslip-update").start()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hands the downloaded APK to Android's installer. The first time, Android wants the driver to
|
* Hands the downloaded APK to Android's installer. The first time, Android wants the driver to
|
||||||
* allow installs from Gearslip; that opens its settings page, and the install carries on when
|
* allow installs from Gearslip; that opens its settings page, and the install carries on when
|
||||||
* they come back.
|
* they come back. A [quiet] install never opens anything: if Android wants a confirmation, it
|
||||||
|
* falls back to the card and the notification.
|
||||||
*/
|
*/
|
||||||
fun install(context: Context) {
|
fun install(context: Context, quiet: Boolean = false) {
|
||||||
val ready = flow.value as? State.Ready ?: return
|
val ready = flow.value as? State.Ready ?: return
|
||||||
// Installing closes Gearslip, which would drop the car screen mid-drive.
|
// Installing closes Gearslip, which would drop the car screen mid-drive.
|
||||||
if (carConnected) return
|
if (carConnected) return
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
if (!pm.canRequestPackageInstalls()) {
|
if (!pm.canRequestPackageInstalls()) {
|
||||||
|
if (quiet) return
|
||||||
installWhenAllowed = true
|
installWhenAllowed = true
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}"))
|
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}"))
|
||||||
@@ -289,14 +358,111 @@ object UpdateChecker {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.logshare", ready.apk)
|
val app = context.applicationContext
|
||||||
context.startActivity(
|
Thread({
|
||||||
Intent(Intent.ACTION_VIEW)
|
runCatching { commit(app, ready, quiet) }.onFailure {
|
||||||
.setDataAndType(uri, "application/vnd.android.package-archive")
|
GearslipLog.w("update: install failed to start: ${it.message}")
|
||||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK),
|
if (!quiet) flow.value = State.Failed("Android couldn't start the install. Try again.", ready.release)
|
||||||
)
|
}
|
||||||
|
}, "gearslip-update").start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the APK into an install session. Android skips its prompt when Gearslip installed the
|
||||||
|
* version being replaced; otherwise [onInstallStatus] gets a screen to show for the driver.
|
||||||
|
*/
|
||||||
|
private fun commit(app: Context, ready: State.Ready, quiet: Boolean) {
|
||||||
|
val installer = app.packageManager.packageInstaller
|
||||||
|
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||||
|
setAppPackageName(app.packageName)
|
||||||
|
setSize(ready.apk.length())
|
||||||
|
setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||||
|
}
|
||||||
|
val id = installer.createSession(params)
|
||||||
|
installer.openSession(id).use { session ->
|
||||||
|
session.openWrite("gearslip.apk", 0, ready.apk.length()).use { out ->
|
||||||
|
ready.apk.inputStream().use { it.copyTo(out) }
|
||||||
|
session.fsync(out)
|
||||||
|
}
|
||||||
|
if (quiet) prefs(app).edit().putString(KEY_SILENT_VERSION, ready.release.version).commit()
|
||||||
|
val status = Intent(app, UpdateReceiver::class.java)
|
||||||
|
.setAction(UpdateReceiver.ACTION_STATUS)
|
||||||
|
.putExtra(EXTRA_QUIET, quiet)
|
||||||
|
.putExtra(EXTRA_VERSION, ready.release.version)
|
||||||
|
// Mutable: the installer adds the outcome to it.
|
||||||
|
val pending = PendingIntent.getBroadcast(app, id, status, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
|
||||||
|
session.commit(pending.intentSender)
|
||||||
|
}
|
||||||
|
GearslipLog.i("update: handed ${ready.release.version} to Android's installer")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the installer said about a session; on success Gearslip is replaced and this never runs. */
|
||||||
|
internal fun onInstallStatus(context: Context, intent: Intent) {
|
||||||
|
val app = context.applicationContext
|
||||||
|
val quiet = intent.getBooleanExtra(EXTRA_QUIET, false)
|
||||||
|
val version = intent.getStringExtra(EXTRA_VERSION)
|
||||||
|
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
|
||||||
|
when (status) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
if (quiet) {
|
||||||
|
// Android wants a tap, maybe because a browser installed this version. Don't
|
||||||
|
// pop a screen up unasked: drop it, and the card and notification take over.
|
||||||
|
GearslipLog.i("update: Android wants $version confirmed; leaving it to the driver")
|
||||||
|
val session = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
|
||||||
|
if (session >= 0) runCatching { app.packageManager.packageInstaller.abandonSession(session) }
|
||||||
|
prefs(app).edit().putString(KEY_SILENT_REFUSED, version).remove(KEY_SILENT_VERSION).apply()
|
||||||
|
val release = foundRelease(app) ?: return
|
||||||
|
if (prefs(app).getString(KEY_NOTIFIED, null) != release.version && notify(app, release)) {
|
||||||
|
prefs(app).edit().putString(KEY_NOTIFIED, release.version).apply()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val confirm = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) ?: return
|
||||||
|
runCatching { app.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PackageInstaller.STATUS_SUCCESS -> GearslipLog.i("update: $version installed")
|
||||||
|
else -> {
|
||||||
|
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
GearslipLog.w("update: install of $version failed ($status, $message)")
|
||||||
|
prefs(app).edit().remove(KEY_SILENT_VERSION).apply()
|
||||||
|
if (quiet) prefs(app).edit().putString(KEY_SILENT_REFUSED, version).apply()
|
||||||
|
else if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||||
|
flow.value = State.Failed("Android didn't install it. Try again.", foundRelease(app))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The new version's first breath after updating itself: say so, since nobody was asked. */
|
||||||
|
internal fun onReplaced(context: Context) {
|
||||||
|
val app = context.applicationContext
|
||||||
|
val version = prefs(app).getString(KEY_SILENT_VERSION, null) ?: return
|
||||||
|
prefs(app).edit().remove(KEY_SILENT_VERSION).apply()
|
||||||
|
if (version != BuildConfig.VERSION_NAME) return
|
||||||
|
GearslipLog.i("update: now on $version, installed without asking")
|
||||||
|
if (app.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) return
|
||||||
|
app.getSystemService(NotificationManager::class.java).createNotificationChannel(
|
||||||
|
NotificationChannel(CHANNEL, "Updates", NotificationManager.IMPORTANCE_DEFAULT),
|
||||||
|
)
|
||||||
|
val open = PendingIntent.getActivity(
|
||||||
|
app, UPDATED_NOTIFICATION_ID,
|
||||||
|
Intent(app, GearslipActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
val notification = NotificationCompat.Builder(app, CHANNEL)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setContentTitle(app.getString(R.string.update_installed_title, version))
|
||||||
|
.setContentText(app.getString(R.string.update_installed_text))
|
||||||
|
.setContentIntent(open)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build()
|
||||||
|
runCatching { NotificationManagerCompat.from(app).notify(UPDATED_NOTIFICATION_ID, notification) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val EXTRA_QUIET = "quiet"
|
||||||
|
private const val EXTRA_VERSION = "version"
|
||||||
|
|
||||||
/** The newest release with an APK that [channel] takes. Drafts never count. */
|
/** The newest release with an APK that [channel] takes. Drafts never count. */
|
||||||
private fun fetchNewest(channel: Channel): Release? {
|
private fun fetchNewest(channel: Channel): Release? {
|
||||||
val stable = channel == Channel.STABLE
|
val stable = channel == Channel.STABLE
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package app.seb3thehacker.gearslip.update
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
/** The installer's answer about an update, and the new version's first start after one. */
|
||||||
|
class UpdateReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_STATUS -> UpdateChecker.onInstallStatus(context, intent)
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> UpdateChecker.onReplaced(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_STATUS = "app.seb3thehacker.gearslip.update.STATUS"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -302,6 +302,10 @@
|
|||||||
<string name="update_check_now">Check now</string>
|
<string name="update_check_now">Check now</string>
|
||||||
<string name="update_check_auto">Check automatically</string>
|
<string name="update_check_auto">Check automatically</string>
|
||||||
<string name="update_once_a_day">Once a day</string>
|
<string name="update_once_a_day">Once a day</string>
|
||||||
|
<string name="update_silent">Install updates by itself</string>
|
||||||
|
<string name="update_silent_sub">Finished releases only, on Wi-Fi, when no car is connected</string>
|
||||||
|
<string name="update_installed_title">Gearslip updated to %1$s</string>
|
||||||
|
<string name="update_installed_text">Installed on Wi-Fi while no car was connected.</string>
|
||||||
|
|
||||||
<!-- -->
|
<!-- -->
|
||||||
<string name="update_unplug_first">Unplug from the car to install. Installing closes Gearslip.</string>
|
<string name="update_unplug_first">Unplug from the car to install. Installing closes Gearslip.</string>
|
||||||
|
|||||||
Reference in new issue
Block a user