diff --git a/app/src/main/kotlin/com/aliucord/manager/di/Http.kt b/app/src/main/kotlin/com/aliucord/manager/di/Http.kt index 81dcb0c..1190c1f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/Http.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/Http.kt @@ -1,18 +1,30 @@ package com.aliucord.manager.di +import android.app.Application import com.aliucord.manager.BuildConfig import io.ktor.client.HttpClient +import io.ktor.client.call.HttpClientCall import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.cache.storage.FileStorage import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.header -import io.ktor.http.HttpHeaders +import io.ktor.client.statement.HttpReceivePipeline +import io.ktor.client.statement.HttpResponse +import io.ktor.http.* import io.ktor.serialization.kotlinx.json.json +import io.ktor.util.AttributeKey +import io.ktor.util.date.GMTDate +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.InternalAPI import kotlinx.serialization.json.Json import okhttp3.Dns import org.koin.core.scope.Scope import java.net.Inet4Address import java.net.InetAddress +import kotlin.coroutines.CoroutineContext @Suppress("UnusedReceiverParameter") fun Scope.provideJson() = Json { @@ -21,10 +33,12 @@ fun Scope.provideJson() = Json { fun Scope.provideHttpClient() = HttpClient(OkHttp) { val json: Json = get() + val application: Application = get() defaultRequest { header(HttpHeaders.UserAgent, "Aliucord Manager/${BuildConfig.VERSION_NAME}") } + engine { config { dns(object : Dns { @@ -42,7 +56,43 @@ fun Scope.provideHttpClient() = HttpClient(OkHttp) { }) } } + install(ContentNegotiation) { json(json) } + + install(HttpCache) { + val dir = application.cacheDir.resolve("ktor") + publicStorage(FileStorage(dir)) + } + + // Custom plugin to allow overriding response cache headers, and force caching + install("OverrideCacheControl") { + receivePipeline.intercept(HttpReceivePipeline.Before) { response -> + val customCacheControl = response.call.attributes.getOrNull(CustomCacheControl) + ?: return@intercept + + proceedWith(object : HttpResponse() { + @InternalAPI + override val rawContent: ByteReadChannel = response.rawContent + override val call: HttpClientCall = response.call + override val coroutineContext: CoroutineContext = response.coroutineContext + override val requestTime: GMTDate = response.requestTime + override val responseTime: GMTDate = response.responseTime + override val status: HttpStatusCode = response.status + override val version: HttpProtocolVersion = response.version + + override val headers: Headers = headers { + appendAll(response.headers) + set(HttpHeaders.CacheControl, customCacheControl.toString()) + } + }) + } + } +} + +private val CustomCacheControl: AttributeKey = AttributeKey("CustomCacheControl") + +fun HttpRequestBuilder.cacheControl(cacheControl: CacheControl) { + attributes.put(CustomCacheControl, cacheControl) } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt index 8bde805..150e1dc 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/download/KtorDownloadManager.kt @@ -27,6 +27,8 @@ class KtorDownloadManager(private val http: HttpClient) : IDownloadManager { try { val httpStmt = http.prepareGet(url) { + header(HttpHeaders.CacheControl, "no-cache, no-store") + // Disable compression due to bug on emulators // This header cannot be set with Android's DownloadManager if (IS_PROBABLY_EMULATOR) { diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt index a29775c..3cfba69 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt @@ -2,17 +2,23 @@ package com.aliucord.manager.network.services import com.aliucord.manager.network.models.BuildInfo import com.aliucord.manager.network.utils.ApiResponse +import io.ktor.client.request.header import io.ktor.client.request.url -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import io.ktor.http.HttpHeaders class AliucordGithubService( val github: GithubService, val http: HttpService, ) { - suspend fun getDataJson(): ApiResponse { - return withContext(Dispatchers.IO) { - http.request { url(DATA_JSON_URL) } + /** + * Fetches the build data of Aliucord (excluding Aliuhook). + * @param force Whether to force Ktor to refetch/revalidate cache. + */ + suspend fun getBuildData(force: Boolean = false): ApiResponse = http.request { + url(DATA_JSON_URL) + + if (force) { + header(HttpHeaders.CacheControl, "no-cache") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt index aed0482..2f19592 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt @@ -1,12 +1,22 @@ package com.aliucord.manager.network.services +import com.aliucord.manager.di.cacheControl import com.aliucord.manager.network.utils.* +import io.ktor.client.request.header import io.ktor.client.request.url -import io.ktor.http.HttpStatusCode +import io.ktor.http.* class AliucordMavenService(private val http: HttpService) { - suspend fun getAliuhookVersion(): ApiResponse { - val metadataResponse = http.request { url(ALIUHOOK_METADATA_URL) } + suspend fun getAliuhookVersion(force: Boolean = false): ApiResponse { + val metadataResponse = http.request { + url(ALIUHOOK_METADATA_URL) + + if (!force) { + cacheControl(CacheControl.MaxAge(maxAgeSeconds = 60 * 30)) // 30 min + } else { + header(HttpHeaders.CacheControl, "no-cache") + } + } return metadataResponse.transform { val versionString = "(.+?)".toRegex() diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt index 87e94a1..193296a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt @@ -35,7 +35,7 @@ class DownloadAliuhookStep : DownloadStep(), IDexProvider, KoinComponent { override suspend fun execute(container: StepRunner) { container.log("Obtaining latest aliuhook version") - targetVersion = maven.getAliuhookVersion().getOrThrow() + targetVersion = maven.getAliuhookVersion(force = true).getOrThrow() container.log("Fetched aliuhook version: $targetVersion") super.execute(container) diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt index c1cf998..1805dde 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt @@ -26,7 +26,7 @@ class FetchInfoStep : Step(), KoinComponent { override suspend fun execute(container: StepRunner) { container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}") - data = github.getDataJson().getOrThrow() + data = github.getBuildData(force = true).getOrThrow() container.log("Fetched build data: $data") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt index 42d3b54..209d519 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt @@ -154,7 +154,7 @@ class HomeModel( } suspend fun fetchRemoteData() { - github.getDataJson().fold( + github.getBuildData().fold( success = { val versionCode = it.discordVersionCode.toIntOrNull() if (versionCode == null) {