diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26ed01f..67c13df 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,7 +20,7 @@ android { minSdk = 24 targetSdk = 34 // 版本号为x.y.z则versionCode为x*1000000+y*10000+z*100+debug版本号(开发需要时迭代, 两位数) - versionCode = 4_04_017 + versionCode = 4_04_020 versionName = "0.4.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadata.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadata.kt index ea9826c..30f681e 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadata.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadata.kt @@ -5,5 +5,6 @@ data class AppCenterMetadata ( val versionName: String, val releaseNotes: String, val downloadUrl: String, - val downloadSize: String + val downloadSize: String, + val checksum: String ) \ No newline at end of file diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadataAdapter.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadataAdapter.kt index f1df495..5f753fc 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadataAdapter.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/AppCenterMetadataAdapter.kt @@ -26,6 +26,7 @@ class AppCenterMetadataAdapter : TypeAdapter() { var releaseNotes: String? = null var downloadUrl: String? = null var downloadSize: String? = null + var checksum: String? = null reader.beginObject() while (reader.hasNext()) { @@ -35,6 +36,7 @@ class AppCenterMetadataAdapter : TypeAdapter() { "release_notes" -> releaseNotes = reader.nextString() "download_url" -> downloadUrl = reader.nextString() "size" -> downloadSize = reader.nextString() + "fingerprint" -> checksum = reader.nextString() else -> reader.skipValue() } } @@ -45,7 +47,8 @@ class AppCenterMetadataAdapter : TypeAdapter() { versionName!!, releaseNotes!!, downloadUrl!!, - downloadSize!! + downloadSize!!, + checksum!! ) } } diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/Release.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/Release.kt index d794349..ac0582b 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/Release.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/Release.kt @@ -6,7 +6,8 @@ data class Release( val versionName: String? = null, val releaseNotes: String? = null, val downloadUrl: String? = null, - val downloadSize: String? = null + val downloadSize: String? = null, + val checksum: String? = null ) enum class ReleaseStatus { diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/UpdateCheckRepository.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/UpdateCheckRepository.kt index f99cfed..f97b3b0 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/UpdateCheckRepository.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/data/update/UpdateCheckRepository.kt @@ -3,10 +3,8 @@ package indi.dmzz_yyhyy.lightnovelreader.data.update import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Environment import android.util.Log import android.widget.Toast -import androidx.core.content.ContextCompat.startActivity import androidx.core.content.FileProvider import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -21,6 +19,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jsoup.Jsoup import java.io.File +import java.io.FileInputStream +import java.security.MessageDigest import javax.inject.Inject import javax.inject.Singleton @@ -61,7 +61,8 @@ class UpdateCheckRepository @Inject constructor( gsonData.versionName, gsonData.releaseNotes, gsonData.downloadUrl, - gsonData.downloadSize + gsonData.downloadSize, + gsonData.checksum ) } else { Log.i("UpdateChecker", "App is up to date") @@ -74,20 +75,24 @@ class UpdateCheckRepository @Inject constructor( } } - fun downloadUpdate(url: String, version: String, size: Long, context: Context) { + fun downloadUpdate(url: String, version: String, checksum: String, context: Context) { val fileName = "LightNovelReader-update-$version.apk" - val downloadPath = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path - val file = File(downloadPath, fileName) + val cacheDir = File(context.cacheDir, "updates") + if (!cacheDir.exists()) cacheDir.mkdirs() + val file = File(cacheDir, fileName) if (url.isBlank()) return if (file.exists()) { - if (file.length() == size) { + if (checkMD5sum(file, checksum)) { installApk(file, context) return - } else file.delete() + } else { + file.delete() + Toast.makeText(context, "本地文件校验和计算失败,正在重新下载...", Toast.LENGTH_SHORT).show() + } } + val ketch: Ketch = Ketch.init( context = context, notificationConfig = NotificationConfig( @@ -100,33 +105,60 @@ class UpdateCheckRepository @Inject constructor( ketch.download( url = url, fileName = fileName, - path = downloadPath, + path = cacheDir.path, tag = "Updates", onSuccess = { - installApk(file, context) + if (checkMD5sum(file, checksum)) { + installApk(file, context) + } else { + file.delete() + Toast.makeText(context, "校验和计算失败,请重试", Toast.LENGTH_SHORT).show() + } }, onFailure = { Toast.makeText(context, "下载失败,请尝试手动下载", Toast.LENGTH_SHORT).show() val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(context, intent, null) + context.startActivity(intent) } ) } } private fun installApk(file: File, context: Context) { + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file) val intent = Intent(Intent.ACTION_VIEW).apply { - setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) - val uri: Uri = - FileProvider.getUriForFile(context, "${context.packageName}.provider", file) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) setDataAndType(uri, "application/vnd.android.package-archive") } context.startActivity(intent) } + private fun checkMD5sum(file: File, checksum: String): Boolean { + val digest = MessageDigest.getInstance("MD5") + val buffer = ByteArray(1024) + + return try { + FileInputStream(file).use { stream -> + var bytesRead: Int + while (stream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + + val result = digest.digest().joinToString("") { "%02x".format(it) } + Log.i("UpdateChecker", "CheckMD5sum result: ${file.path}\n[file] $result -> $checksum [expected checksum]") + + result.equals(checksum, ignoreCase = true) + } catch (e: Exception) { + Log.e("UpdateChecker", "Error checking MD5 sum", e) + false + } + } + private fun createGson(): Gson { return GsonBuilder() .registerTypeAdapter(AppCenterMetadata::class.java, AppCenterMetadataAdapter()) .create() } + } \ No newline at end of file diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderApp.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderApp.kt index e441b0f..e893e58 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderApp.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderApp.kt @@ -58,19 +58,25 @@ fun LightNovelReaderApp( val navController = rememberNavController() AnimatedVisibility(visible = viewModel.updateDialogUiState.visible) { + val releaseNotes = viewModel.updateDialogUiState.release.releaseNotes ?: "" + val downloadUrl = viewModel.updateDialogUiState.release.downloadUrl ?: "" + val version = viewModel.updateDialogUiState.release.version ?: -1 + val versionName = viewModel.updateDialogUiState.release.versionName ?: "" + val checksum = viewModel.updateDialogUiState.release.checksum ?: "" + val downloadSize = viewModel.updateDialogUiState.release.downloadSize?.toLong() ?: -1 UpdatesAvailableDialog( onDismissRequest = viewModel::onDismissUpdateRequest, onConfirmation = { viewModel.downloadUpdate( - url = viewModel.updateDialogUiState.release.downloadUrl ?: "", - version = viewModel.updateDialogUiState.release.versionName ?: "", - size = viewModel.updateDialogUiState.release.downloadSize?.toLong() ?: -1, + url = downloadUrl, + version = versionName, + checksum = checksum, context = context ) }, - newVersionCode = viewModel.updateDialogUiState.release.version ?: -1, - newVersionName = viewModel.updateDialogUiState.release.versionName ?: "", - contentMarkdown = viewModel.updateDialogUiState.release.releaseNotes ?: "", - downloadSize = viewModel.updateDialogUiState.release.downloadSize ?: "", - downloadUrl = viewModel.updateDialogUiState.release.downloadUrl + newVersionCode = version, + newVersionName = versionName, + contentMarkdown = releaseNotes, + downloadSize = downloadSize.toDouble(), + downloadUrl = downloadUrl ) } AnimatedVisibility(visible = viewModel.addToBookshelfDialogUiState.visible) { diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderViewModel.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderViewModel.kt index 83e3442..d75fbe1 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderViewModel.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/LightNovelReaderViewModel.kt @@ -92,8 +92,8 @@ class LightNovelReaderViewModel @Inject constructor( } } - fun downloadUpdate(url: String, version: String, size: Long, context: Context) = - updateCheckRepository.downloadUpdate(url, version, size, context) + fun downloadUpdate(url: String, version: String, checksum: String, context: Context) = + updateCheckRepository.downloadUpdate(url, version, checksum, context) fun clearToast() { _updateDialogUiState.toast = "" diff --git a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/Dialog.kt b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/Dialog.kt index 56ff20f..7ec5836 100644 --- a/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/Dialog.kt +++ b/app/src/main/kotlin/indi/dmzz_yyhyy/lightnovelreader/ui/components/Dialog.kt @@ -146,7 +146,7 @@ fun UpdatesAvailableDialog( contentMarkdown: String? = null, newVersionName: String? = null, newVersionCode: Int = 0, - downloadSize: String? = null, + downloadSize: Double? = null, downloadUrl: String? = null ) { val context = LocalContext.current @@ -160,7 +160,7 @@ fun UpdatesAvailableDialog( text = { Column { newVersionName?.let { - val sizeInMB = ((downloadSize?.toDoubleOrNull() ?: 0.0) / 1024) / 1024 + val sizeInMB = ((downloadSize ?: 0.0) / 1024) / 1024 val formatted = "%.2f".format(sizeInMB) Text( text = "${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) → $newVersionName($newVersionCode), ${formatted}MB" diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index fafa14f..f9649fc 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,6 +1,4 @@ - - \ No newline at end of file + +