Skip to content

Commit

Permalink
Add update check for FOSS builds
Browse files Browse the repository at this point in the history
  • Loading branch information
d4rken committed Sep 3, 2024
1 parent f02dee3 commit c43cdd0
Show file tree
Hide file tree
Showing 41 changed files with 1,216 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.octi.common.serialization.adapter.ByteStringAdapter
import eu.darken.octi.common.serialization.adapter.DurationAdapter
import eu.darken.octi.common.serialization.adapter.InstantAdapter
import eu.darken.octi.common.serialization.adapter.LocaleAdapter
import eu.darken.octi.common.serialization.adapter.OffsetDateTimeAdapter
import eu.darken.octi.common.serialization.adapter.RegexAdapter
import eu.darken.octi.common.serialization.adapter.UUIDAdapter
import eu.darken.octi.common.serialization.adapter.UriAdapter
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
Expand All @@ -14,8 +22,13 @@ class SerializationModule {
@Provides
@Singleton
fun moshi(): Moshi = Moshi.Builder().apply {
add(ByteStringAdapter())
add(DurationAdapter())
add(InstantAdapter())
add(LocaleAdapter())
add(OffsetDateTimeAdapter())
add(RegexAdapter())
add(UriAdapter())
add(UUIDAdapter())
add(ByteStringAdapter())
}.build()
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.darken.octi.common.serialization
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.time.Duration

class DurationAdapter {
@ToJson
fun toJson(value: Duration): String = value.toString()

@FromJson
fun fromJson(raw: String): Duration = Duration.parse(raw)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.darken.octi.common.serialization
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.util.Locale

class LocaleAdapter {
@ToJson
fun toJson(value: Locale): String = value.toLanguageTag()

@FromJson
fun fromJson(raw: String) = Locale.forLanguageTag(raw)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter

class OffsetDateTimeAdapter {
@ToJson
fun toJson(value: OffsetDateTime): String = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value)

@FromJson
fun fromJson(value: String): OffsetDateTime = OffsetDateTime.parse(value, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson

class RegexAdapter {

@ToJson
fun toJson(value: Regex): Wrapper = Wrapper(
pattern = value.pattern,
options = value.options.map { it.toWrapperOption() }.toSet(),
)

@FromJson
fun fromJson(raw: Wrapper): Regex = Regex(
pattern = raw.pattern,
options = raw.options.map { it.toRegexOption() }.toSet()
)

private fun Wrapper.Option.toRegexOption() = when (this) {
Wrapper.Option.IGNORE_CASE -> RegexOption.IGNORE_CASE
Wrapper.Option.MULTILINE -> RegexOption.MULTILINE
Wrapper.Option.LITERAL -> RegexOption.LITERAL
Wrapper.Option.UNIX_LINES -> RegexOption.UNIX_LINES
Wrapper.Option.COMMENTS -> RegexOption.COMMENTS
Wrapper.Option.DOT_MATCHES_ALL -> RegexOption.DOT_MATCHES_ALL
Wrapper.Option.CANON_EQ -> RegexOption.CANON_EQ
}

private fun RegexOption.toWrapperOption() = when (this) {
RegexOption.IGNORE_CASE -> Wrapper.Option.IGNORE_CASE
RegexOption.MULTILINE -> Wrapper.Option.MULTILINE
RegexOption.LITERAL -> Wrapper.Option.LITERAL
RegexOption.UNIX_LINES -> Wrapper.Option.UNIX_LINES
RegexOption.COMMENTS -> Wrapper.Option.COMMENTS
RegexOption.DOT_MATCHES_ALL -> Wrapper.Option.DOT_MATCHES_ALL
RegexOption.CANON_EQ -> Wrapper.Option.CANON_EQ
}

@JsonClass(generateAdapter = true)
data class Wrapper(
@Json(name = "pattern") val pattern: String,
@Json(name = "options") val options: Set<Option>
) {
@JsonClass(generateAdapter = false)
enum class Option {
@Json(name = "IGNORE_CASE") IGNORE_CASE,
@Json(name = "MULTILINE") MULTILINE,
@Json(name = "LITERAL") LITERAL,
@Json(name = "UNIX_LINES") UNIX_LINES,
@Json(name = "COMMENTS") COMMENTS,
@Json(name = "DOT_MATCHES_ALL") DOT_MATCHES_ALL,
@Json(name = "CANON_EQ") CANON_EQ,
;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package eu.darken.octi.common.serialization
package eu.darken.octi.common.serialization.adapter

import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.util.*
import java.util.UUID

class UUIDAdapter {
@ToJson
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package eu.darken.octi.common.serialization.adapter

import android.net.Uri
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson

class UriAdapter {
@ToJson
fun toJson(uri: Uri): String = uri.toString()

@FromJson
fun fromJson(uriString: String): Uri = Uri.parse(uriString)
}
1 change: 1 addition & 0 deletions app-common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<string name="general_manage_action">Manage</string>
<string name="general_quota_label">Quota</string>
<string name="general_upgrade_action">Upgrade</string>
<string name="general_update_action">Update</string>
<string name="general_donate_action">Donate</string>
<string name="general_check_action">Check</string>
<string name="general_internal_not_available_msg">Internet connection unavailable</string>
Expand Down
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ android {
val variantOutputImpl = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
val variantName: String = variantOutputImpl.name

if (listOf("release", "beta").any { variantName.toLowerCase().contains(it) }) {
if (listOf("release", "beta").any { variantName.lowercase().contains(it) }) {
val outputFileName = packageName +
"-v${defaultConfig.versionName}-${defaultConfig.versionCode}" +
"-${variantName.toUpperCase()}.apk"
"-${variantName.uppercase()}.apk"

variantOutputImpl.outputFileName = outputFileName
}
Expand Down Expand Up @@ -172,4 +172,6 @@ dependencies {
"gplayImplementation"("com.android.billingclient:billing-ktx:7.0.0")

implementation("io.coil-kt:coil:2.0.0-rc02")

implementation("io.github.z4kn4fein:semver:1.4.2")
}
117 changes: 117 additions & 0 deletions app/src/foss/java/eu/darken/octi/main/core/FossUpdateChecker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package eu.darken.octi.main.core

import dagger.Reusable
import eu.darken.octi.common.WebpageTool
import eu.darken.octi.common.datastore.value
import eu.darken.octi.common.debug.logging.Logging.Priority.ERROR
import eu.darken.octi.common.debug.logging.Logging.Priority.INFO
import eu.darken.octi.common.debug.logging.Logging.Priority.WARN
import eu.darken.octi.common.debug.logging.asLog
import eu.darken.octi.common.debug.logging.log
import eu.darken.octi.common.debug.logging.logTag
import eu.darken.octi.main.core.updater.UpdateChecker
import java.time.Duration
import java.time.Instant
import javax.inject.Inject

@Reusable
class FossUpdateChecker @Inject constructor(
private val checker: GithubReleaseCheck,
private val webpageTool: WebpageTool,
private val settings: FossUpdateSettings,
) : UpdateChecker {

override suspend fun getLatest(channel: UpdateChecker.Channel): UpdateChecker.Update? {
log(TAG) { "getLatest($channel) checking..." }

val release: GithubApi.ReleaseInfo? = try {
if (Duration.between(settings.lastReleaseCheck.value(), Instant.now()) < UPDATE_CHECK_INTERVAL) {
log(TAG) { "Using cached release data" }
when (channel) {
UpdateChecker.Channel.BETA -> settings.lastReleaseBeta.value()
UpdateChecker.Channel.PROD -> settings.lastReleaseProd.value()
}
} else {
log(TAG) { "Fetching new release data" }
when (channel) {
UpdateChecker.Channel.BETA -> checker.allReleases(OWNER, REPO).first()
UpdateChecker.Channel.PROD -> checker.latestRelease(OWNER, REPO)
}.also {
log(TAG, INFO) { "getLatest($channel) new data is $it" }
settings.lastReleaseCheck.value(Instant.now())
when (channel) {
UpdateChecker.Channel.BETA -> settings.lastReleaseBeta.value(it)
UpdateChecker.Channel.PROD -> settings.lastReleaseProd.value(it)
}
}
}
} catch (e: Exception) {
log(TAG, ERROR) { "getLatest($channel) failed: ${e.asLog()}" }
null
}

log(TAG, INFO) { "getLatest($channel) is ${release?.tagName}" }

val update = release?.let { rel ->
Update(
channel = channel,
versionName = rel.tagName,
changelogLink = rel.htmlUrl,
downloadLink = rel.assets.singleOrNull { it.name.endsWith(".apk") }?.downloadUrl,
)
}

return update
}

override suspend fun startUpdate(update: UpdateChecker.Update) {
log(TAG, INFO) { "startUpdate($update)" }
update as Update
if (update.downloadLink != null) {
webpageTool.open(update.downloadLink)
} else {
log(TAG, WARN) { "No download link available for $update" }
}
}

override suspend fun viewUpdate(update: UpdateChecker.Update) {
log(TAG, INFO) { "viewUpdate($update)" }
update as Update
webpageTool.open(update.changelogLink)
}

override suspend fun dismissUpdate(update: UpdateChecker.Update) {
log(TAG, INFO) { "dismissUpdate($update)" }
update as Update
settings.dismiss(update)
}

override suspend fun isDismissed(update: UpdateChecker.Update): Boolean {
update as Update
return settings.isDismissed(update)
}

override fun isEnabledByDefault(): Boolean {
val isEnabled = false
log(TAG, INFO) { "Update check default isEnabled=$isEnabled" }
return isEnabled
}

override suspend fun isCheckSupported(): Boolean {
return true
}

data class Update(
override val channel: UpdateChecker.Channel,
override val versionName: String,
val changelogLink: String,
val downloadLink: String?,
) : UpdateChecker.Update

companion object {
private val UPDATE_CHECK_INTERVAL = Duration.ofHours(6)
private const val OWNER = "d4rken-org"
private const val REPO = "octi"
private val TAG = logTag("Updater", "Checker", "FOSS")
}
}
47 changes: 47 additions & 0 deletions app/src/foss/java/eu/darken/octi/main/core/FossUpdateSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package eu.darken.octi.main.core

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.octi.common.datastore.DataStoreValue
import eu.darken.octi.common.datastore.createValue
import eu.darken.octi.common.datastore.value
import eu.darken.octi.common.debug.logging.logTag
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class FossUpdateSettings @Inject constructor(
@ApplicationContext private val context: Context,
moshi: Moshi,
) {

private val Context.dataStore by preferencesDataStore(name = "settings_updater_foss")

private val dataStore: DataStore<Preferences>
get() = context.dataStore

private fun FossUpdateChecker.Update.getSetting(): DataStoreValue<Boolean> {
return dataStore.createValue("update.${this.versionName}.dismissed", false)
}

suspend fun dismiss(update: FossUpdateChecker.Update) {
update.getSetting().value(true)
}

suspend fun isDismissed(update: FossUpdateChecker.Update): Boolean {
return update.getSetting().value()
}

val lastReleaseCheck = dataStore.createValue("check.last", Instant.EPOCH, moshi)
val lastReleaseProd = dataStore.createValue<GithubApi.ReleaseInfo?>("check.last.prod", null, moshi)
val lastReleaseBeta = dataStore.createValue<GithubApi.ReleaseInfo?>("check.last.beta", null, moshi)

companion object {
private val TAG = logTag("Updater", "Checker", "FOSS", "Settings")
}
}
Loading

0 comments on commit c43cdd0

Please sign in to comment.