1 Commits
Author SHA1 Message Date
OneWay e8a49575c1 Initial ReSearch export 2026-07-06 09:29:50 +08:00
9 changed files with 30 additions and 113 deletions
+3 -3
View File
@@ -92,8 +92,8 @@ android {
applicationId = "com.example.research" applicationId = "com.example.research"
minSdk = project.property("minSdk").toString().toInt() minSdk = project.property("minSdk").toString().toInt()
targetSdk = project.property("targetSdk").toString().toInt() targetSdk = project.property("targetSdk").toString().toInt()
versionCode = 2 versionCode = 1
versionName = "1.1.0" versionName = "1.0"
val isFdroid = project.hasProperty("fdroid") val isFdroid = project.hasProperty("fdroid")
if (isFdroid) { if (isFdroid) {
@@ -153,7 +153,7 @@ android {
aboutLibraries { aboutLibraries {
collect { collect {
fetchRemoteLicense = false fetchRemoteLicense = true
fetchRemoteFunding = false fetchRemoteFunding = false
configPath = file("config") configPath = file("config")
} }
-38
View File
@@ -32,41 +32,3 @@ HSPLcom/example/research/ui/article/ArticleState**->**(**)**
# About screen (bonus optimization) # About screen (bonus optimization)
HSPLcom/example/research/ui/about/AboutScreenKt**->**(**)** HSPLcom/example/research/ui/about/AboutScreenKt**->**(**)**
# Updated 2026-07-11: cover the settings/dictionary flow and the Compose framework.
# Settings + dictionary management flow
HSPLcom/example/research/ui/settings/**->**(**)**
Lcom/example/research/ui/settings/**
HSPLcom/example/research/common/ui/components/**->**(**)**
Lcom/example/research/common/ui/components/**
HSPLcom/example/research/common/progress/**->**(**)**
Lcom/example/research/common/progress/**
HSPLcom/example/research/feature/download/**->**(**)**
Lcom/example/research/feature/download/**
# Pre-verify the app classes already covered by method rules above
Lcom/example/research/**
# Compose framework: the alpha AARs ship near-empty baseline profiles, so first
# compositions run interpreted without these rules.
HSPLandroidx/compose/runtime/**->**(**)**
Landroidx/compose/runtime/**
HSPLandroidx/compose/ui/**->**(**)**
Landroidx/compose/ui/**
HSPLandroidx/compose/foundation/**->**(**)**
Landroidx/compose/foundation/**
HSPLandroidx/compose/material3/**->**(**)**
Landroidx/compose/material3/**
HSPLandroidx/compose/animation/**->**(**)**
Landroidx/compose/animation/**
HSPLandroidx/compose/material/**->**(**)**
Landroidx/compose/material/**
# Support libraries on the same cold path
HSPLandroidx/lifecycle/**->**(**)**
Landroidx/lifecycle/**
HSPLandroidx/activity/compose/**->**(**)**
Landroidx/activity/compose/**
HSPLkotlinx/coroutines/**->**(**)**
Lkotlinx/coroutines/**
@@ -94,8 +94,7 @@ class DictionaryChecker(
.build() .build()
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
response.isSuccessful && response.isSuccessful
response.header("Content-Type")?.startsWith("text/html") != true
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Failed to check source availability for ${source.urlTemplate}: ${e.message}") Log.w(TAG, "Failed to check source availability for ${source.urlTemplate}: ${e.message}")
@@ -33,8 +33,6 @@ import kotlin.time.Duration.Companion.seconds
class HttpException(val statusCode: Int, message: String) : IOException(message) class HttpException(val statusCode: Int, message: String) : IOException(message)
class InvalidPayloadException(message: String) : IOException(message)
class DictionaryDownloader( class DictionaryDownloader(
private val context: Context, private val context: Context,
private val client: OkHttpClient, private val client: OkHttpClient,
@@ -46,9 +44,6 @@ class DictionaryDownloader(
private val RETRY_DELAY_BASE = 1.seconds private val RETRY_DELAY_BASE = 1.seconds
private val MAX_RETRY_DELAY = 10.seconds private val MAX_RETRY_DELAY = 10.seconds
private val FILE_CREATION_RETRY_DELAY = 200.milliseconds private val FILE_CREATION_RETRY_DELAY = 200.milliseconds
private fun isGzipHeader(buffer: ByteArray, length: Int): Boolean =
length >= 2 && buffer[0] == 0x1f.toByte() && buffer[1] == 0x8b.toByte()
} }
private suspend fun deletePartialFileSafely( private suspend fun deletePartialFileSafely(
@@ -144,9 +139,7 @@ class DictionaryDownloader(
} }
val error = currentResult.exceptionOrNull() val error = currentResult.exceptionOrNull()
val currentBuildMissing = (error is HttpException && error.statusCode == 404) || if (hasDatePlaceholder && error is HttpException && error.statusCode == 404) {
error is InvalidPayloadException
if (hasDatePlaceholder && currentBuildMissing) {
val fallbackUrl = DictionarySource.buildUrl(source.urlTemplate, fallbackDate) val fallbackUrl = DictionarySource.buildUrl(source.urlTemplate, fallbackDate)
val fallbackFileName = DictionarySource.extractFileName(source.urlTemplate, fallbackDate) val fallbackFileName = DictionarySource.extractFileName(source.urlTemplate, fallbackDate)
return@withContext downloadFile( return@withContext downloadFile(
@@ -241,13 +234,6 @@ class DictionaryDownloader(
ensureActive() ensureActive()
val body = response.body val body = response.body
val expectGzip = fileName.lowercase().endsWith(DictionaryConfig.GZ_EXTENSION)
if (expectGzip && body.contentType()?.type == "text") {
response.close()
return@withContext Result.failure(
InvalidPayloadException(context.getString(R.string.download_file_not_found))
)
}
val contentLength = body.contentLength() val contentLength = body.contentLength()
fileUri = fileStorageManager.createFileInFolder(folderUri, fileName, overwrite = true) fileUri = fileStorageManager.createFileInFolder(folderUri, fileName, overwrite = true)
if (fileUri == null) { if (fileUri == null) {
@@ -275,9 +261,6 @@ class DictionaryDownloader(
while (input.read(buffer).also { bytesRead = it } != -1) { while (input.read(buffer).also { bytesRead = it } != -1) {
ensureActive() ensureActive()
if (expectGzip && totalBytesRead == 0L && bytesRead > 0 && !isGzipHeader(buffer, bytesRead)) {
throw InvalidPayloadException(context.getString(R.string.download_file_not_found))
}
output.write(buffer, 0, bytesRead) output.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead totalBytesRead += bytesRead
if (contentLength > 0) { if (contentLength > 0) {
@@ -318,11 +301,6 @@ class DictionaryDownloader(
deletePartialFileSafely(fileUri, folderUri, fileName) deletePartialFileSafely(fileUri, folderUri, fileName)
throw e throw e
} }
if (e is InvalidPayloadException) {
deletePartialFileIfExists(folderUri, fileName)
deletePartialFileSafely(fileUri, folderUri, fileName)
return@withContext Result.failure(e)
}
if (e.isNetworkError() && currentRetry < maxRetries) { if (e.isNetworkError() && currentRetry < maxRetries) {
deletePartialFileIfExists(folderUri, fileName) deletePartialFileIfExists(folderUri, fileName)
deletePartialFileSafely(fileUri, folderUri, fileName) deletePartialFileSafely(fileUri, folderUri, fileName)
@@ -86,14 +86,8 @@ class DictionaryRepository(
} }
val allSources = preferencesManager.dictionarySources.first() val allSources = preferencesManager.dictionarySources.first()
val sourceKeys = sourceUrls.mapTo(mutableSetOf(), DictionarySource::identityKey) val normalizedSourceUrls = sourceUrls.map { DictionarySource.normalizeTemplate(it) }
val sourcesToDownload = allSources.mapNotNull { source -> val sourcesToDownload = allSources.filter { it.urlTemplate in normalizedSourceUrls && it.isEnabled }
if (!source.isEnabled || DictionarySource.identityKey(source.urlTemplate) !in sourceKeys) {
null
} else {
source.copy(urlTemplate = DictionarySource.normalizeTemplate(source.urlTemplate))
}
}
if (sourcesToDownload.isEmpty()) { if (sourcesToDownload.isEmpty()) {
return@withContext Result.success(Unit) return@withContext Result.success(Unit)
@@ -29,28 +29,13 @@ fun DictionaryProgressSection(
percent: Int, percent: Int,
testTag: String, testTag: String,
) { ) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
modifier = modifier
) {
val targetProgress = progressProvider().coerceIn(0f, 1f) val targetProgress = progressProvider().coerceIn(0f, 1f)
var monotonicTargetProgress by remember { mutableFloatStateOf(0f) } var monotonicTargetProgress by remember { mutableFloatStateOf(0f) }
var displayedLabel by remember { mutableStateOf(label) }
var displayedPercent by remember { mutableIntStateOf(percent) }
var displayedTestTag by remember { mutableStateOf(testTag) }
LaunchedEffect(visible, targetProgress) { LaunchedEffect(visible, targetProgress) {
if (visible) { monotonicTargetProgress = if (visible) {
monotonicTargetProgress = maxOf(monotonicTargetProgress, targetProgress) maxOf(monotonicTargetProgress, targetProgress)
} } else {
} 0f
SideEffect {
if (visible) {
displayedLabel = label
displayedPercent = percent
displayedTestTag = testTag
} }
} }
val animatedProgress by animateFloatAsState( val animatedProgress by animateFloatAsState(
@@ -59,18 +44,24 @@ fun DictionaryProgressSection(
label = "dictionary-progress" label = "dictionary-progress"
) )
Column(modifier = Modifier.testTag(displayedTestTag)) { AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
modifier = modifier.testTag(testTag)
) {
Column {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = displayedLabel, text = label,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text( Text(
text = "$displayedPercent%", text = "$percent%",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@@ -1,5 +0,0 @@
- Dictionary updates: a missing daily build served as an HTML page no longer breaks the update; the app now falls back to the previous day's build
- Fixed update check reporting an update when none is available on the server
- Fixed source matching when a dictionary source was added with a dated URL
- Fixed the progress bar animating backwards when a download or indexing finishes
- Faster first opening of screens (ahead-of-time compilation profile now covers the whole UI)
@@ -1,5 +0,0 @@
- Обновление словарей: отсутствующая суточная сборка, отдаваемая как HTML-страница, больше не ломает обновление — приложение переключается на сборку за предыдущий день
- Исправлена проверка обновлений, сообщавшая об обновлении, которого нет на сервере
- Исправлено сопоставление источников, добавленных по URL с датой
- Исправлена обратная анимация индикатора прогресса при завершении загрузки и индексации
- Ускорено первое открытие экранов (AOT-профиль компиляции покрывает весь интерфейс)
+4 -1
View File
@@ -8,7 +8,6 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError
# org.gradle.java.home - commented out to use system JAVA_HOME or auto-provisioning # org.gradle.java.home - commented out to use system JAVA_HOME or auto-provisioning
# org.gradle.java.home=/Users/evgenii/.gradle/jdks/eclipse_adoptium-17-aarch64-os_x.2/jdk-17.0.15+6/Contents/Home
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
@@ -41,6 +40,10 @@ kotlin.daemon.jvmargs=-Xmx3g -XX:+UseParallelGC
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
# Android build optimizations
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
# SDK Versions # SDK Versions
compileSdk=37 compileSdk=37
minSdk=31 minSdk=31