Skip to content

Commit 288adea

Browse files
committed
refactor(app): enhance auto-app updater
Removed dependency off of flixclusiveorg@flixclusive-config repository to get stable app updates.
1 parent f6ad0fe commit 288adea

File tree

17 files changed

+306
-137
lines changed

17 files changed

+306
-137
lines changed

core/network/src/main/kotlin/com/flixclusive/core/network/retrofit/GithubApiService.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ interface GithubApiService {
2424
): GithubBranchInfo
2525

2626
/**
27-
* Retrieves the release notes for the given tag.
27+
* Retrieves the release for the given tag.
2828
*
2929
* @param tag The tag name.
3030
* @return A [GithubReleaseInfo] object.
@@ -34,10 +34,17 @@ interface GithubApiService {
3434
@Path("tag") tag: String
3535
): GithubReleaseInfo
3636

37+
/**
38+
* Retrieves the latest stable release for the given tag.
39+
*
40+
* @return A [GithubReleaseInfo] object.
41+
*/
42+
@GET("repos/$GITHUB_USERNAME/$GITHUB_REPOSITORY/releases/latest")
43+
suspend fun getStableReleaseInfo(): GithubReleaseInfo
44+
3745
/**
3846
* Retrieves the release notes for the given tag.
3947
*
40-
* @param tag The tag name.
4148
* @return A [GithubReleaseInfo] object.
4249
*/
4350
@GET("repos/$GITHUB_USERNAME/$GITHUB_REPOSITORY/tags")

core/network/src/main/kotlin/com/flixclusive/core/network/retrofit/GithubRawApiService.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.flixclusive.core.network.retrofit
22

33
import com.flixclusive.core.util.common.GithubConstant.GITHUB_CONFIG_REPOSITORY
44
import com.flixclusive.core.util.common.GithubConstant.GITHUB_USERNAME
5-
import com.flixclusive.model.configuration.AppConfig
65
import com.flixclusive.model.configuration.catalog.HomeCatalogsData
76
import com.flixclusive.model.configuration.catalog.SearchCatalogsData
87
import retrofit2.http.GET
@@ -20,6 +19,4 @@ interface GithubRawApiService {
2019
@GET("$GITHUB_USERNAME/$GITHUB_CONFIG_REPOSITORY/main/search_items_config.json")
2120
suspend fun getSearchCatalogsConfig(): SearchCatalogsData
2221

23-
@GET("$GITHUB_USERNAME/$GITHUB_CONFIG_REPOSITORY/main/app.json")
24-
suspend fun getAppConfig(): AppConfig
2522
}

data/configuration/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ android {
99
}
1010

1111
dependencies {
12-
api(projects.core.datastore)
1312
api(libs.stubs.util)
13+
api(projects.core.datastore)
1414
api(projects.model.configuration)
1515

1616
implementation(libs.mockk)
1717
implementation(projects.core.locale)
1818
implementation(projects.core.network)
19+
20+
21+
testImplementation(libs.retrofit.gson)
1922
}
Lines changed: 21 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
package com.flixclusive.data.configuration
22

33
import com.flixclusive.core.datastore.AppSettingsManager
4-
import com.flixclusive.core.locale.UiText
5-
import com.flixclusive.core.network.retrofit.GithubApiService
64
import com.flixclusive.core.network.retrofit.GithubRawApiService
75
import com.flixclusive.core.network.util.Resource
86
import com.flixclusive.core.network.util.Resource.Failure.Companion.toNetworkException
9-
import com.flixclusive.core.util.common.GithubConstant.GITHUB_REPOSITORY
10-
import com.flixclusive.core.util.common.GithubConstant.GITHUB_USERNAME
117
import com.flixclusive.core.util.coroutines.AppDispatchers
8+
import com.flixclusive.core.util.coroutines.AppDispatchers.Companion.launchOnIO
129
import com.flixclusive.core.util.log.errorLog
1310
import com.flixclusive.core.util.network.okhttp.UserAgentManager
14-
import com.flixclusive.model.configuration.AppConfig
1511
import com.flixclusive.model.configuration.catalog.HomeCatalogsData
1612
import com.flixclusive.model.configuration.catalog.SearchCatalogsData
1713
import kotlinx.coroutines.Job
@@ -27,16 +23,6 @@ import javax.inject.Inject
2723
import javax.inject.Singleton
2824
import com.flixclusive.core.locale.R as LocaleR
2925

30-
sealed class UpdateStatus(
31-
val errorMessage: UiText? = null
32-
) {
33-
data object Fetching: UpdateStatus()
34-
data object Maintenance : UpdateStatus()
35-
data object Outdated : UpdateStatus()
36-
data object UpToDate : UpdateStatus()
37-
class Error(errorMessage: UiText?) : UpdateStatus(errorMessage)
38-
}
39-
4026
/**
4127
*
4228
* Substitute model for BuildConfig
@@ -55,7 +41,7 @@ private const val MAX_RETRIES = 5
5541
@Singleton
5642
class AppConfigurationManager @Inject constructor(
5743
private val githubRawApiService: GithubRawApiService,
58-
private val githubApiService: GithubApiService,
44+
private val appUpdateChecker: AppUpdateChecker,
5945
private val appSettingsManager: AppSettingsManager,
6046
client: OkHttpClient,
6147
) {
@@ -72,16 +58,17 @@ class AppConfigurationManager @Inject constructor(
7258
var currentAppBuild: AppBuild? = null
7359
private set
7460

75-
var appConfig: AppConfig? = null
61+
var appUpdateInfo: AppUpdateInfo? = null
7662
var homeCatalogsData: HomeCatalogsData? = null
7763
var searchCatalogsData: SearchCatalogsData? = null
7864

7965
private val Resource<Unit>.needsToInitialize: Boolean
8066
get() = (this is Resource.Success
81-
&& (appConfig == null || homeCatalogsData == null || searchCatalogsData == null))
67+
&& (appUpdateInfo == null || homeCatalogsData == null || searchCatalogsData == null))
68+
|| this is Resource.Failure
8269

8370
init {
84-
AppDispatchers.Default.scope.launch {
71+
launchOnIO {
8572
_configurationStatus.collectLatest {
8673
if(it.needsToInitialize)
8774
initialize(currentAppBuild)
@@ -93,10 +80,10 @@ class AppConfigurationManager @Inject constructor(
9380
if(fetchJob?.isActive == true)
9481
return
9582

96-
if(this.currentAppBuild == null)
97-
this.currentAppBuild = appBuild
83+
if(currentAppBuild == null)
84+
currentAppBuild = appBuild
9885

99-
fetchJob = AppDispatchers.Default.scope.launch {
86+
fetchJob = AppDispatchers.IO.scope.launch {
10087
val retryDelay = 3000L
10188
for (i in 0..MAX_RETRIES) {
10289
_configurationStatus.update { Resource.Loading }
@@ -135,58 +122,26 @@ class AppConfigurationManager @Inject constructor(
135122
val appSettings = appSettingsManager.appSettings.data.first()
136123
val isUsingPrereleaseUpdates = appSettings.isUsingPrereleaseUpdates
137124

138-
appConfig = githubRawApiService.getAppConfig()
139-
140-
if(appConfig!!.isMaintenance)
141-
return _updateStatus.update { UpdateStatus.Maintenance }
142-
143-
if (isUsingPrereleaseUpdates && currentAppBuild?.debug == false) {
144-
val lastCommitObject = githubApiService.getLastCommitObject()
145-
val appCommitVersion = currentAppBuild?.commitVersion
146-
?: throw NullPointerException("appCommitVersion should not be null!")
147-
148-
val preReleaseTag = "pre-release"
149-
val preReleaseTagInfo = githubApiService.getTagsInfo().find { it.name == preReleaseTag }
150-
151-
val shortenedSha = lastCommitObject.lastCommit.sha.shortenSha()
152-
val isNeedingAnUpdate = appCommitVersion != shortenedSha
153-
&& lastCommitObject.lastCommit.sha == preReleaseTagInfo?.lastCommit?.sha
154-
155-
if (isNeedingAnUpdate) {
156-
val preReleaseReleaseInfo = githubApiService.getReleaseInfo(tag = preReleaseTag)
157-
158-
appConfig = appConfig!!.copy(
159-
versionName = "PR-$shortenedSha \uD83D\uDDFF",
160-
updateInfo = preReleaseReleaseInfo.releaseNotes,
161-
updateUrl = "https://github.com/$GITHUB_USERNAME/$GITHUB_REPOSITORY/releases/download/pre-release/flixclusive-release.apk"
162-
)
163-
164-
_updateStatus.update { UpdateStatus.Outdated }
165-
return
166-
}
167-
168-
_updateStatus.update { UpdateStatus.UpToDate }
169-
return
125+
val status = if (isUsingPrereleaseUpdates && currentAppBuild?.debug == false) {
126+
appUpdateChecker.checkForPrereleaseUpdates(
127+
currentAppBuild = currentAppBuild!!
128+
)
170129
} else {
171-
val isNeedingAnUpdate = appConfig!!.build != -1L && appConfig!!.build > currentAppBuild!!.build
172-
173-
if(isNeedingAnUpdate) {
174-
val releaseInfo = githubApiService.getReleaseInfo(tag = appConfig!!.versionName)
175-
176-
appConfig = appConfig!!.copy(updateInfo = releaseInfo.releaseNotes)
177-
return _updateStatus.update { UpdateStatus.Outdated }
178-
}
130+
appUpdateChecker.checkForStableUpdates(
131+
currentAppBuild = currentAppBuild!!
132+
)
133+
}
179134

180-
return _updateStatus.update { UpdateStatus.UpToDate }
135+
if (status is UpdateStatus.Outdated) {
136+
appUpdateInfo = status.updateInfo
181137
}
138+
139+
_updateStatus.update { status }
182140
} catch (e: Exception) {
183141
errorLog(e)
184142
val errorMessageId = e.toNetworkException().error!!
185143

186144
_updateStatus.update { UpdateStatus.Error(errorMessageId) }
187145
}
188146
}
189-
190-
private fun String.shortenSha()
191-
= substring(0, 7)
192147
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.flixclusive.data.configuration
2+
3+
import com.flixclusive.core.network.retrofit.GithubApiService
4+
import com.flixclusive.core.util.common.GithubConstant
5+
import retrofit2.HttpException
6+
import java.time.Instant
7+
import javax.inject.Inject
8+
9+
internal const val PRE_RELEASE_TAG = "pre-release"
10+
11+
class AppUpdateChecker @Inject constructor(
12+
private val githubApiService: GithubApiService,
13+
) {
14+
suspend fun checkForPrereleaseUpdates(currentAppBuild: AppBuild): UpdateStatus {
15+
return safeNetworkCall(currentAppBuild) { currentAppUpdateInfo ->
16+
val lastCommitObject = githubApiService.getLastCommitObject()
17+
val appCommitVersion = currentAppBuild.commitVersion
18+
19+
val preReleaseTagInfo =
20+
githubApiService.getTagsInfo().find { it.name == PRE_RELEASE_TAG }
21+
22+
val shortenedSha = lastCommitObject.lastCommit.shortSha
23+
val isNeedingAnUpdate = appCommitVersion != shortenedSha
24+
&& lastCommitObject.lastCommit.sha == preReleaseTagInfo?.lastCommit?.sha
25+
26+
if (isNeedingAnUpdate) {
27+
val preReleaseReleaseInfo = githubApiService.getReleaseInfo(tag = PRE_RELEASE_TAG)
28+
29+
val newAppConfig = AppUpdateInfo(
30+
versionName = "PR-$shortenedSha \uD83D\uDDFF",
31+
updateInfo = preReleaseReleaseInfo.releaseNotes,
32+
updateUrl = "https://github.com/${GithubConstant.GITHUB_USERNAME}/${GithubConstant.GITHUB_REPOSITORY}/releases/download/$PRE_RELEASE_TAG/flixclusive-release.apk"
33+
)
34+
35+
return UpdateStatus.Outdated(updateInfo = newAppConfig)
36+
}
37+
38+
return UpdateStatus.UpToDate(updateInfo = currentAppUpdateInfo)
39+
}
40+
}
41+
42+
suspend fun checkForStableUpdates(currentAppBuild: AppBuild): UpdateStatus {
43+
return safeNetworkCall(currentAppBuild) { currentAppUpdateInfo ->
44+
val latestStableRelease = githubApiService.getStableReleaseInfo()
45+
val currentReleaseInfo =
46+
githubApiService.getReleaseInfo(tag = currentAppBuild.versionName)
47+
48+
val latestReleaseCreationDate =
49+
Instant.parse(latestStableRelease.createdAt).toEpochMilli()
50+
val currentReleaseCreationDate =
51+
Instant.parse(currentReleaseInfo.createdAt).toEpochMilli()
52+
53+
val latestSemVer = parseSemVer(latestStableRelease.name)
54+
val currentSemVer = parseSemVer(currentAppBuild.versionName)
55+
56+
val isNeedingAnUpdate = latestReleaseCreationDate > currentReleaseCreationDate
57+
&& latestSemVer > currentSemVer
58+
59+
if (isNeedingAnUpdate) {
60+
val newAppUpdateInfo = AppUpdateInfo(
61+
versionName = latestStableRelease.name,
62+
updateInfo = latestStableRelease.releaseNotes,
63+
updateUrl = "https://github.com/${GithubConstant.GITHUB_USERNAME}/${GithubConstant.GITHUB_REPOSITORY}/releases/download/${latestStableRelease.name}/flixclusive-release.apk"
64+
)
65+
66+
return UpdateStatus.Outdated(updateInfo = newAppUpdateInfo)
67+
}
68+
69+
return UpdateStatus.UpToDate(updateInfo = currentAppUpdateInfo)
70+
}
71+
}
72+
73+
private inline fun safeNetworkCall(
74+
currentAppBuild: AppBuild,
75+
block: (AppUpdateInfo) -> UpdateStatus
76+
): UpdateStatus {
77+
val currentAppUpdateInfo = AppUpdateInfo(
78+
versionName = currentAppBuild.versionName,
79+
updateUrl = "https://github.com/${GithubConstant.GITHUB_USERNAME}/${GithubConstant.GITHUB_REPOSITORY}/releases/download/${currentAppBuild.versionName}/flixclusive-release.apk"
80+
)
81+
82+
try {
83+
return block.invoke(currentAppUpdateInfo)
84+
} catch (e: HttpException) {
85+
val body = e.response()?.errorBody()?.string()
86+
if (e.code() == 404 || body?.contains("Not Found") == true) {
87+
return UpdateStatus.UpToDate(updateInfo = currentAppUpdateInfo)
88+
}
89+
90+
throw e
91+
}
92+
}
93+
94+
private fun parseSemVer(version: String): SemanticVersion {
95+
val regex = Regex("""(\d+)\.(\d+)\.(\d+)""")
96+
val match = regex.find(version)
97+
98+
return match?.let {
99+
val (major, minor, patch) = it.destructured
100+
SemanticVersion(major.toInt(), minor.toInt(), patch.toInt())
101+
} ?: SemanticVersion(
102+
major = -1,
103+
minor = -1,
104+
patch = -1
105+
)
106+
}
107+
108+
private data class SemanticVersion(
109+
val major: Int,
110+
val minor: Int,
111+
val patch: Int
112+
) : Comparable<SemanticVersion> {
113+
override fun compareTo(other: SemanticVersion): Int {
114+
// Compare major versions
115+
if (this.major != other.major) {
116+
return this.major.compareTo(other.major)
117+
}
118+
119+
// Compare minor versions
120+
if (this.minor != other.minor) {
121+
return this.minor.compareTo(other.minor)
122+
}
123+
124+
// Compare patch versions
125+
return this.patch.compareTo(other.patch)
126+
}
127+
}
128+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.flixclusive.data.configuration
2+
3+
data class AppUpdateInfo(
4+
val versionName: String,
5+
val updateUrl: String,
6+
val updateInfo: String? = null,
7+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.flixclusive.data.configuration
2+
3+
import com.flixclusive.core.locale.UiText
4+
5+
sealed class UpdateStatus(
6+
val errorMessage: UiText? = null
7+
) {
8+
data object Fetching : UpdateStatus()
9+
data class Outdated(val updateInfo: AppUpdateInfo) : UpdateStatus()
10+
data class UpToDate(val updateInfo: AppUpdateInfo) : UpdateStatus()
11+
class Error(errorMessage: UiText?) : UpdateStatus(errorMessage)
12+
}

data/configuration/src/main/kotlin/com/flixclusive/data/configuration/di/test/TestAppConfigurationModule.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package com.flixclusive.data.configuration.di.test
22

33
import com.flixclusive.core.util.network.json.fromJson
44
import com.flixclusive.data.configuration.AppConfigurationManager
5+
import com.flixclusive.data.configuration.AppUpdateInfo
56
import com.flixclusive.data.configuration.di.test.constant.APP_CONFIG
67
import com.flixclusive.data.configuration.di.test.constant.HOME_CATEGORIES
78
import com.flixclusive.data.configuration.di.test.constant.SEARCH_CATEGORIES
8-
import com.flixclusive.model.configuration.AppConfig
99
import com.flixclusive.model.configuration.catalog.HomeCatalogsData
1010
import com.flixclusive.model.configuration.catalog.SearchCatalogsData
1111
import io.mockk.every
@@ -19,12 +19,12 @@ object TestAppConfigurationModule {
1919
fun getMockAppConfigurationManager(): AppConfigurationManager {
2020
val homeCatalogsDataMock = fromJson<HomeCatalogsData>(HOME_CATEGORIES)
2121
val searchCatalogsDataMock = fromJson<SearchCatalogsData>(SEARCH_CATEGORIES)
22-
val appConfigMock = fromJson<AppConfig>(APP_CONFIG)
22+
val appUpdateInfoMock = fromJson<AppUpdateInfo>(APP_CONFIG)
2323

2424
val mock = mockk<AppConfigurationManager> {
2525
every { homeCatalogsData } returns homeCatalogsDataMock
2626
every { searchCatalogsData } returns searchCatalogsDataMock
27-
every { appConfig } returns appConfigMock
27+
every { appUpdateInfo } returns appUpdateInfoMock
2828
}
2929

3030
return mock

0 commit comments

Comments
 (0)