Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Version data class #36

Merged
merged 13 commits into from
Jan 18, 2024
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ android {
applicationId = "com.chouten.app"
minSdk = 23
targetSdk = 34
versionCode = 10
versionName = "0.3.0"
versionCode = 11
versionName = "0.3.1"
buildConfigField("String", "WEBHOOK_URL", "\"${properties.getProperty("bug_webhook")}\"")

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/chouten/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.SideEffect
import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider
import com.chouten.app.common.compareSemVer
import com.chouten.app.common.findActivity
import com.chouten.app.domain.model.AlertDialogModel
import com.chouten.app.domain.model.LogEntry
import com.chouten.app.domain.model.SnackbarModel
import com.chouten.app.domain.model.Version
import com.chouten.app.domain.proto.moduleDatastore
import com.chouten.app.domain.use_case.log_use_cases.LogUseCases
import com.chouten.app.domain.use_case.module_use_cases.ModuleInstallEvent
Expand Down Expand Up @@ -83,7 +83,7 @@ class MainActivity : ComponentActivity() {
moduleUseCases.addModule(updateUrl.toUri()) { event ->
when (event) {
is ModuleInstallEvent.PARSED -> {
(event.module.version.compareSemVer(module.version) != 1).also { res ->
(event.module.version < module.version).also { res ->
if (!res) {
appState.viewModel.runAsync {
logUseCases.insertLog(
Expand Down
40 changes: 9 additions & 31 deletions app/src/main/java/com/chouten/app/common/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import com.chouten.app.domain.model.Version
import com.chouten.app.domain.proto.AppearancePreferences
import com.chouten.app.domain.proto.appearanceDatastore
import kotlinx.coroutines.flow.firstOrNull
Expand Down Expand Up @@ -137,7 +138,7 @@ fun Long.formatMinSec(): String {
return if (this <= 0L) {
"00:00"
} else {
// Format HH:MM::SS or MM:SS if hours is 0
// Format HH:MM:SS or MM:SS if hours is 0
if (TimeUnit.MILLISECONDS.toHours(this) > 0) {
String.format(
"%02d:%02d:%02d",
Expand Down Expand Up @@ -200,34 +201,11 @@ fun calculateFraction(start: Float, end: Float, pos: Float) =
(if (end - start == 0f) 0f else (pos - start) / (end - start)).coerceIn(0f, 1f)

/**
* Compare two semVer versions
* @param v2 The second version
* @return 1 if the first version is newer, -1 if the second version is newer, 0 if the versions are the same
* @throws IllegalArgumentException if the versions are invalid
* Parses a version string into a [Version] object.
*
* @param useRegex Flag to determine whether to use regex for parsing.
* Not using regex is stricter and will throw an exception for more invalid strings.
* @return The parsed [Version] object.
* @throws IllegalArgumentException If the version string is not valid.
*/
fun String.compareSemVer(v2: String): Int {
// Return 1 if the first version is newer
// Return -1 if the second version is newer
// Return 0 if the versions are the same
val v1Split = split(".")
val v2Split = v2.split(".")
if (v1Split.size != 3 || v2Split.size != 3) {
throw IllegalArgumentException("Invalid version")
}

return if (v1Split[0].toInt() > v2Split[0].toInt()) {
1
} else if (v1Split[0].toInt() < v2Split[0].toInt()) {
-1
} else if (v1Split[1].toInt() > v2Split[1].toInt()) {
1
} else if (v1Split[1].toInt() < v2Split[1].toInt()) {
-1
} else if (v1Split[2].toInt() > v2Split[2].toInt()) {
1
} else if (v1Split[2].toInt() < v2Split[2].toInt()) {
-1
} else {
0
}
}
fun String.toVersion(useRegex: Boolean = false) = Version(this, useRegex)
15 changes: 13 additions & 2 deletions app/src/main/java/com/chouten/app/common/TimeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import java.util.Locale
import java.util.TimeZone

/**
* Converts epoch seconds to a formatted time string.
* Converts epoch milliseconds to a formatted time string.
*
* @param epochMilli The epoch time in seconds.
* @param epochMilli The epoch time in milliseconds.
* @param pattern The desired pattern for formatting the time (default is "h:mm:ss").
* @return The formatted time string of [epochMilli] in the pattern of [pattern].
*/
Expand All @@ -28,4 +28,15 @@ fun epochMillisToTime(epochMilli: Long, pattern: String = "HH:mm:ss"): String {
timeZone = userTimeZone
}.format(Date(epochMilli))
}
}

/**
* Converts epoch milliseconds to a formatted time string.
*
* @param epochMilli The epoch time in milliseconds.
* @param pattern The desired pattern for formatting the time (default is "h:mm:ss").
* @return The formatted time string of [epochMilli] in the pattern of [pattern].
*/
fun Long.toTimeString(pattern: String = "HH:mm:ss"): String {
return epochMillisToTime(this, pattern)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ModuleRepositoryImpl @Inject constructor(
override suspend fun getModuleDirs(): List<Uri> {

val preferences = context.filepathDatastore.data.first()
if (preferences.CHOUTEN_ROOT_DIR == Uri.EMPTY) return emptyList()
val contentResolver = context.contentResolver

return withContext(Dispatchers.IO) {
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/java/com/chouten/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,13 @@ object AppModule {
@Provides
fun provideModuleRepository(app: Application, httpClient: Requests): ModuleRepository {
val moduleDirGetter: suspend (Uri) -> Uri = { uri: Uri ->
GetModuleDirUseCase(
app.applicationContext
)(uri)
try {
GetModuleDirUseCase(
app.applicationContext
)(uri)
} catch (e: Exception) {
Uri.EMPTY
}
}
return ModuleRepositoryImpl(app.applicationContext, moduleDirGetter)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ data class ModuleModel(
* The version of the module.
* This is used to identify the module in the app.
*/
val version: String,
val version: Version,

/**
* The format version for the code of the module.
Expand Down
183 changes: 183 additions & 0 deletions app/src/main/java/com/chouten/app/domain/model/SemanticVersion.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.chouten.app.domain.model

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* Serializer for [Version].
*/
object VersionAsStringSerializer : KSerializer<Version> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Version", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Version) {
return encoder.encodeString(value.toString())
}

override fun deserialize(decoder: Decoder): Version {
return Version(decoder.decodeString())
}
}

/**
* Represents a semantic version number.
*
* @property major The major version number, used to indicate incompatible API changes.
* @property minor The minor version number, used to indicate new, but backwards compatible, functionality.
* @property patch The patch version number, used to indicate backwards compatible bug fixes and small changes.
* @property preRelease The pre-release identifier.
* @property buildMetadata The build metadata.
*/
@Serializable(with = VersionAsStringSerializer::class)
data class Version(
var major: Int,
var minor: Int,
var patch: Int,
var preRelease: String = "",
var buildMetadata: String = ""
) : Comparable<Version> {

/**
* Creates a [Version] object from a version string.
*
* @param versionString The version string to parse.
* @param useRegex Flag to determine whether to use regex for parsing.
* Not using regex is stricter and will throw an exception for more invalid strings.
* @throws IllegalArgumentException If the version string is not valid.
*/
constructor(versionString: String, useRegex: Boolean = false) : this(
major = 0, minor = 0, patch = 0
) {
val parsedVersion = parse(versionString, useRegex)
major = parsedVersion.major
minor = parsedVersion.minor
patch = parsedVersion.patch
preRelease = parsedVersion.preRelease
buildMetadata = parsedVersion.buildMetadata
}

companion object {
private val SEMVER_REGEX = Regex(
"(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?"
)

/**
* Parses a version string into a [Version] object.
*
* @param versionString The version string to parse.
* @param useRegex Flag to determine whether to use regex for parsing.
* Not using regex is stricter and will throw an exception for more invalid strings.
* @return The parsed [Version] object.
* @throws IllegalArgumentException If the version string is not valid.
*/
fun parse(versionString: String, useRegex: Boolean = false): Version {
if (useRegex) {
val matchResult = SEMVER_REGEX.matchEntire(versionString)
?: throw IllegalArgumentException("Invalid semantic version format")

return Version(
major = matchResult.groupValues[1].toInt(),
minor = matchResult.groupValues[2].toInt(),
patch = matchResult.groupValues[3].toInt(),
preRelease = matchResult.groupValues[4],
buildMetadata = matchResult.groupValues[5]
)
} else {
val metadataSplit = versionString.split("+", limit = 2)
val mainPart = metadataSplit[0]
val buildMetadata = if (metadataSplit.size > 1) metadataSplit[1] else ""

val preReleaseSplit = mainPart.split("-", limit = 2)
val numbers = preReleaseSplit[0].split(".")
val preRelease = if (preReleaseSplit.size > 1) preReleaseSplit[1] else ""

if (numbers.size != 3) {
throw IllegalArgumentException("Invalid version format. Expected format: MAJOR.MINOR.PATCH")
}

val major = numbers[0].toIntOrNull()
?: throw IllegalArgumentException("Major version is not a valid integer")
val minor = numbers[1].toIntOrNull()
?: throw IllegalArgumentException("Minor version is not a valid integer")
val patch = numbers[2].toIntOrNull()
?: throw IllegalArgumentException("Patch version is not a valid integer")

return Version(major, minor, patch, preRelease, buildMetadata)
}
}
}

/**
* Compares this version with the specified version for order.
*
* @param other The [Version] to be compared.
* @return A negative integer if this version is less than the other version,
* zero if they are equal or a positive integer if this version is greater than the other version.
*/
override fun compareTo(other: Version): Int {
if (this.major != other.major) return this.major - other.major
if (this.minor != other.minor) return this.minor - other.minor
if (this.patch != other.patch) return this.patch - other.patch
if (this.preRelease != other.preRelease) {
if (this.preRelease.isEmpty()) return 1
if (other.preRelease.isEmpty()) return -1
// Split preRelease strings and compare them part by part.
val thisPreReleaseParts = this.preRelease.split(".")
val otherPreReleaseParts = other.preRelease.split(".")
val maxIndex = minOf(thisPreReleaseParts.size, otherPreReleaseParts.size)
for (i in 0 until maxIndex) {
val cmp = thisPreReleaseParts[i].compareTo(otherPreReleaseParts[i])
if (cmp != 0) return cmp
}
return thisPreReleaseParts.size - otherPreReleaseParts.size
}
// Note: Build metadata does not affect version precedence
return 0
}

/**
* Checks if this version is equal to the specified version.
*
* @param other The [Version] to compare.
* @return `true` if the versions are equal, `false` otherwise.
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Version

if (major != other.major) return false
if (minor != other.minor) return false
if (patch != other.patch) return false
if (preRelease != other.preRelease) return false

return true
}

/**
* @return a hash code value for the version.
*/
override fun hashCode(): Int {
var result = major
result = 31 * result + minor
result = 31 * result + patch
result = 31 * result + preRelease.hashCode()
result = 31 * result + buildMetadata.hashCode()
return result
}

/**
* Returns a string representation of the version.
*/
override fun toString(): String {
return "$major.$minor.$patch" +
(if (preRelease.isNotEmpty()) "-$preRelease" else "") +
(if (buildMetadata.isNotEmpty()) "+$buildMetadata" else "")
}
}
Loading