diff --git a/app/build.gradle b/app/build.gradle
index 8bcf265..8748e50 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,8 +21,8 @@ android {
applicationId "me.ash.reader"
minSdk 26
targetSdk 32
- versionCode 5
- versionName "0.7.2"
+ versionCode 6
+ versionName "0.7.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 517a222..5be8d32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="me.ash.reader">
+
+
+ tools:node="remove" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt
index 64f1e0c..7e5a522 100644
--- a/app/src/main/java/me/ash/reader/App.kt
+++ b/app/src/main/java/me/ash/reader/App.kt
@@ -15,9 +15,7 @@ import me.ash.reader.data.source.AppNetworkDataSource
import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.data.source.ReaderDatabase
import me.ash.reader.data.source.RssNetworkDataSource
-import me.ash.reader.ui.ext.DataStoreKeys
-import me.ash.reader.ui.ext.dataStore
-import me.ash.reader.ui.ext.put
+import me.ash.reader.ui.ext.*
import javax.inject.Inject
@HiltAndroidApp
@@ -99,7 +97,12 @@ class App : Application(), Configuration.Provider {
}
private suspend fun checkUpdate() {
- appRepository.checkUpdate()
+ applicationContext.getLatestApk().let {
+ if (it.exists()) {
+ it.del()
+ }
+ }
+ appRepository.checkUpdate(showToast = false)
}
override fun getWorkManagerConfiguration(): Configuration =
diff --git a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt
index 0697c43..5d67c56 100644
--- a/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt
+++ b/app/src/main/java/me/ash/reader/data/repository/AppRepository.kt
@@ -5,12 +5,17 @@ import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.withContext
import me.ash.reader.R
import me.ash.reader.data.entity.toVersion
import me.ash.reader.data.module.ApplicationScope
import me.ash.reader.data.module.DispatcherIO
+import me.ash.reader.data.module.DispatcherMain
import me.ash.reader.data.source.AppNetworkDataSource
+import me.ash.reader.data.source.Download
+import me.ash.reader.data.source.downloadToFileWithProgress
import me.ash.reader.ui.ext.*
import javax.inject.Inject
@@ -22,11 +27,28 @@ class AppRepository @Inject constructor(
private val applicationScope: CoroutineScope,
@DispatcherIO
private val dispatcherIO: CoroutineDispatcher,
+ @DispatcherMain
+ private val dispatcherMain: CoroutineDispatcher,
) {
- suspend fun checkUpdate(): Boolean = withContext(dispatcherIO) {
+ suspend fun checkUpdate(showToast: Boolean = true): Boolean? = withContext(dispatcherIO) {
try {
- val latest =
+ val response =
appNetworkDataSource.getReleaseLatest(context.getString(R.string.update_link))
+ when {
+ response.code() == 403 -> {
+ withContext(dispatcherMain) {
+ if (showToast) context.showToast(context.getString(R.string.rate_limit))
+ }
+ return@withContext null
+ }
+ response.body() == null -> {
+ withContext(dispatcherMain) {
+ if (showToast) context.showToast(context.getString(R.string.check_failure))
+ }
+ return@withContext null
+ }
+ }
+ val latest = response.body()!!
val latestVersion = latest.tag_name.toVersion()
// val latestVersion = "0.7.3".toVersion()
val skipVersion = context.skipVersionNumber.toVersion()
@@ -60,7 +82,26 @@ class AppRepository @Inject constructor(
} catch (e: Exception) {
e.printStackTrace()
Log.e("RLog", "checkUpdate: ${e.message}")
- false
+ withContext(dispatcherMain) {
+ if (showToast) context.showToast(context.getString(R.string.check_failure))
+ }
+ null
}
}
+
+ suspend fun downloadFile(url: String): Flow =
+ withContext(dispatcherIO) {
+ Log.i("RLog", "downloadFile start: $url")
+ try {
+ return@withContext appNetworkDataSource.downloadFile(url)
+ .downloadToFileWithProgress(context.getLatestApk())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ Log.e("RLog", "downloadFile: ${e.message}")
+ withContext(dispatcherMain) {
+ context.showToast(context.getString(R.string.download_failure))
+ }
+ }
+ emptyFlow()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt b/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt
index 1935615..a37d951 100644
--- a/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt
+++ b/app/src/main/java/me/ash/reader/data/source/AppNetworkDataSource.kt
@@ -1,14 +1,33 @@
package me.ash.reader.data.source
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
import me.ash.reader.data.entity.LatestRelease
+import okhttp3.ResponseBody
+import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
+import retrofit2.http.Streaming
import retrofit2.http.Url
+import java.io.File
+
+sealed class Download {
+ object NotYet : Download()
+ data class Progress(val percent: Int) : Download()
+ data class Finished(val file: File) : Download()
+}
interface AppNetworkDataSource {
@GET
- suspend fun getReleaseLatest(@Url url: String): LatestRelease
+ suspend fun getReleaseLatest(@Url url: String): Response
+
+ @GET
+ @Streaming
+ suspend fun downloadFile(@Url url: String): ResponseBody
companion object {
private var instance: AppNetworkDataSource? = null
@@ -24,4 +43,53 @@ interface AppNetworkDataSource {
}
}
}
-}
\ No newline at end of file
+}
+
+fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow =
+ flow {
+ emit(Download.Progress(0))
+
+ // flag to delete file if download errors or is cancelled
+ var deleteFile = true
+
+ try {
+ byteStream().use { inputStream ->
+ saveFile.outputStream().use { outputStream ->
+ val totalBytes = contentLength()
+ val data = ByteArray(8_192)
+ var progressBytes = 0L
+
+ while (true) {
+ val bytes = inputStream.read(data)
+
+ if (bytes == -1) {
+ break
+ }
+
+ outputStream.channel
+ outputStream.write(data, 0, bytes)
+ progressBytes += bytes
+
+ emit(Download.Progress(percent = ((progressBytes * 100) / totalBytes).toInt()))
+ }
+
+ when {
+ progressBytes < totalBytes ->
+ throw Exception("missing bytes")
+ progressBytes > totalBytes ->
+ throw Exception("too many bytes")
+ else ->
+ deleteFile = false
+ }
+ }
+ }
+
+ emit(Download.Finished(saveFile))
+ } finally {
+ // check if download was successful
+
+ if (deleteFile) {
+ saveFile.delete()
+ }
+ }
+ }.flowOn(Dispatchers.IO).distinctUntilChanged()
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt
index 1085a9a..203e575 100644
--- a/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt
+++ b/app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt
@@ -3,9 +3,13 @@ package me.ash.reader.ui.ext
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
+import android.content.Intent
+import android.util.Log
import android.widget.Toast
+import androidx.core.content.FileProvider
import me.ash.reader.data.entity.Version
import me.ash.reader.data.entity.toVersion
+import java.io.File
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
@@ -18,6 +22,27 @@ fun Context.getCurrentVersion(): Version = packageManager
.versionName
.toVersion()
+fun Context.getLatestApk(): File = File(cacheDir, "latest.apk")
+
+fun Context.getFileProvider(): String = "${packageName}.fileprovider"
+
+fun Context.installLatestApk() {
+ try {
+ val contentUri = FileProvider.getUriForFile(this, getFileProvider(), getLatestApk())
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ setDataAndType(contentUri, "application/vnd.android.package-archive")
+ }
+ if (packageManager.queryIntentActivities(intent, 0).size > 0) {
+ startActivity(intent)
+ }
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ Log.e("RLog", "installLatestApk: ${e.message}")
+ }
+}
+
private var toast: Toast? = null
fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
diff --git a/app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt b/app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
new file mode 100644
index 0000000..b0e20f2
--- /dev/null
+++ b/app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
@@ -0,0 +1,38 @@
+package me.ash.reader.ui.ext
+
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStream
+import java.io.InputStreamReader
+
+/**
+ * Convert [InputStream] to [String].
+ */
+fun InputStream.readString(): String =
+ BufferedReader(InputStreamReader(this)).useLines { lines ->
+ val results = StringBuilder()
+ lines.forEach { results.append(it) }
+ results.toString()
+ }
+
+/**
+ * Delete a file or directory.
+ */
+fun File.del() {
+ if (this.isFile) {
+ delete()
+ return
+ }
+ this.listFiles()?.forEach { it.del() }
+ delete()
+}
+
+fun File.mkDir() {
+ val dirArray = this.absolutePath.split("/".toRegex())
+ var pathTemp = ""
+ for (i in 1 until dirArray.size) {
+ pathTemp = "$pathTemp/${dirArray[i]}"
+ val newF = File("${dirArray[0]}$pathTemp")
+ if (!newF.exists()) newF.mkdir()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
index baa85a1..dda4f4f 100644
--- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
@@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.map
import me.ash.reader.R
@@ -33,9 +34,9 @@ import me.ash.reader.ui.theme.palette.onLight
@Composable
fun SettingsPage(
navController: NavHostController,
+ updateViewModel: UpdateViewModel = hiltViewModel(),
) {
val context = LocalContext.current
- var updateDialogVisible by remember { mutableStateOf(false) }
val skipVersion = context.dataStore.data
.map { it[DataStoreKeys.SkipVersionNumber.key] ?: "" }
.collectAsState(initial = "")
@@ -92,7 +93,9 @@ fun SettingsPage(
contentDescription = stringResource(R.string.close),
)
},
- ) { updateDialogVisible = true }
+ ) {
+ updateViewModel.dispatch(UpdateViewAction.Show)
+ }
}
Banner(
title = stringResource(R.string.in_coding),
@@ -148,8 +151,5 @@ fun SettingsPage(
}
)
- UpdateDialog(
- visible = updateDialogVisible,
- onDismissRequest = { updateDialogVisible = false },
- )
+ UpdateDialog()
}
diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt b/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt
index ccd18fb..b4d1992 100644
--- a/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/settings/TipsAndSupport.kt
@@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Balance
+import androidx.compose.material.icons.rounded.TipsAndUpdates
import androidx.compose.material.icons.rounded.VolunteerActivism
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -30,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
@@ -50,13 +52,8 @@ fun TipsAndSupport(
val context = LocalContext.current
val view = LocalView.current
val scope = rememberCoroutineScope()
- val viewState = updateViewModel.viewState.collectAsStateValue()
- val githubLink = stringResource(R.string.github_link)
- val telegramLink = stringResource(R.string.telegram_link)
- val checkingUpdates = stringResource(R.string.checking_updates)
- val isLatestVersion = stringResource(R.string.is_latest_version)
- val comingSoon = stringResource(R.string.coming_soon)
var currentVersion by remember { mutableStateOf("") }
+ var clickTime by remember { mutableStateOf(System.currentTimeMillis() - 2000) }
var pressAMP by remember { mutableStateOf(16f) }
val animatedPress by animateFloatAsState(
targetValue = pressAMP,
@@ -88,7 +85,16 @@ fun TipsAndSupport(
navController.popBackStack()
}
},
- actions = {}
+ actions = {
+ FeedbackIconButton(
+ modifier = Modifier.size(20.dp),
+ imageVector = Icons.Rounded.Balance,
+ contentDescription = stringResource(R.string.open_source_licenses),
+ tint = MaterialTheme.colorScheme.onSurface
+ ) {
+ context.showToast(context.getString(R.string.coming_soon))
+ }
+ }
)
},
content = {
@@ -110,21 +116,30 @@ fun TipsAndSupport(
pressAMP = 16f
},
onTap = {
- context.showToast(checkingUpdates)
- scope.launch {
- updateViewModel.dispatch(
- UpdateViewAction.CheckUpdate(
- {
- context.dataStore.put(
- DataStoreKeys.SkipVersionNumber,
- ""
- )
- },
- {
- if (!it) context.showToast(isLatestVersion)
- }
+ if (System.currentTimeMillis() - clickTime > 2000) {
+ clickTime = System.currentTimeMillis()
+ context.showToast(context.getString(R.string.checking_updates))
+ scope.launch {
+ updateViewModel.dispatch(
+ UpdateViewAction.CheckUpdate(
+ {
+ context.dataStore.put(
+ DataStoreKeys.SkipVersionNumber,
+ ""
+ )
+ },
+ {
+ if (!it) {
+ context.showToast(
+ context.getString(R.string.is_latest_version)
+ )
+ }
+ }
+ )
)
- )
+ }
+ } else {
+ clickTime = System.currentTimeMillis()
}
}
)
@@ -186,22 +201,7 @@ fun TipsAndSupport(
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
- context.showToast(comingSoon)
- })
- Spacer(modifier = Modifier.width(16.dp))
-
- // Telegram
- RoundIconButton(RoundIconButtonType.Telegram(
- backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true,
- ) {
- view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
- view.playSoundEffect(SoundEffectConstants.CLICK)
- context.startActivity(
- Intent(
- Intent.ACTION_VIEW,
- Uri.parse(telegramLink)
- )
- )
+ context.showToast(context.getString(R.string.coming_soon))
})
Spacer(modifier = Modifier.width(16.dp))
@@ -214,19 +214,34 @@ fun TipsAndSupport(
context.startActivity(
Intent(
Intent.ACTION_VIEW,
- Uri.parse(githubLink)
+ Uri.parse(context.getString(R.string.github_link))
)
)
})
Spacer(modifier = Modifier.width(16.dp))
- // License
- RoundIconButton(RoundIconButtonType.License(
+ // Telegram
+ RoundIconButton(RoundIconButtonType.Telegram(
+ backgroundColor = MaterialTheme.colorScheme.primaryContainer alwaysLight true,
+ ) {
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+ view.playSoundEffect(SoundEffectConstants.CLICK)
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(context.getString(R.string.telegram_link))
+ )
+ )
+ })
+ Spacer(modifier = Modifier.width(16.dp))
+
+ // Help
+ RoundIconButton(RoundIconButtonType.Help(
backgroundColor = MaterialTheme.colorScheme.secondaryContainer alwaysLight true,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
view.playSoundEffect(SoundEffectConstants.CLICK)
- context.showToast(comingSoon)
+ context.showToast(context.getString(R.string.coming_soon))
})
}
Spacer(modifier = Modifier.height(48.dp))
@@ -235,10 +250,7 @@ fun TipsAndSupport(
}
)
- UpdateDialog(
- visible = viewState.updateDialogVisible,
- onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
- )
+ UpdateDialog()
}
@Immutable
@@ -247,6 +259,7 @@ sealed class RoundIconButtonType(
val iconVector: ImageVector? = null,
val descResource: Int? = null,
val descString: String? = null,
+ open val size: Dp = 24.dp,
open val offset: Modifier = Modifier.offset(),
open val backgroundColor: Color = Color.Unspecified,
open val onClick: () -> Unit = {},
@@ -263,6 +276,18 @@ sealed class RoundIconButtonType(
onClick = onClick,
)
+ @Immutable
+ data class GitHub(
+ val desc: String = "GitHub",
+ override val backgroundColor: Color,
+ override val onClick: () -> Unit = {},
+ ) : RoundIconButtonType(
+ iconResource = R.drawable.ic_github,
+ descString = desc,
+ backgroundColor = backgroundColor,
+ onClick = onClick,
+ )
+
@Immutable
data class Telegram(
val desc: String = "Telegram",
@@ -277,24 +302,13 @@ sealed class RoundIconButtonType(
)
@Immutable
- data class GitHub(
- val desc: String = "GitHub",
+ data class Help(
+ val desc: Int = R.string.help,
+ override val offset: Modifier = Modifier.offset(x = (3).dp),
override val backgroundColor: Color,
override val onClick: () -> Unit = {},
) : RoundIconButtonType(
- iconResource = R.drawable.ic_github,
- descString = desc,
- backgroundColor = backgroundColor,
- onClick = onClick,
- )
-
- @Immutable
- data class License(
- val desc: Int = R.string.open_source_licenses,
- override val backgroundColor: Color,
- override val onClick: () -> Unit = {},
- ) : RoundIconButtonType(
- iconVector = Icons.Rounded.Balance,
+ iconVector = Icons.Rounded.TipsAndUpdates,
descResource = desc,
backgroundColor = backgroundColor,
onClick = onClick,
@@ -313,9 +327,9 @@ private fun RoundIconButton(type: RoundIconButtonType) {
onClick = { type.onClick() }
) {
when (type) {
- is RoundIconButtonType.Sponsor, is RoundIconButtonType.License -> {
+ is RoundIconButtonType.Sponsor, is RoundIconButtonType.Help -> {
Icon(
- modifier = type.offset,
+ modifier = type.offset.size(type.size),
imageVector = type.iconVector!!,
contentDescription = stringResource(type.descResource!!),
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
@@ -323,7 +337,7 @@ private fun RoundIconButton(type: RoundIconButtonType) {
}
is RoundIconButtonType.GitHub, is RoundIconButtonType.Telegram -> {
Icon(
- modifier = type.offset,
+ modifier = type.offset.size(type.size),
painter = painterResource(type.iconResource!!),
contentDescription = type.descString,
tint = MaterialTheme.colorScheme.onSurface alwaysLight true,
diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt
index d9d0529..66bc3e8 100644
--- a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateDialog.kt
@@ -1,6 +1,13 @@
package me.ash.reader.ui.page.settings
+import android.Manifest
import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@@ -22,26 +29,25 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.ash.reader.R
+import me.ash.reader.data.source.Download
import me.ash.reader.ui.component.Dialog
-import me.ash.reader.ui.ext.DataStoreKeys
-import me.ash.reader.ui.ext.dataStore
-import me.ash.reader.ui.ext.put
+import me.ash.reader.ui.ext.*
@SuppressLint("FlowOperatorInvokedInComposition")
@OptIn(ExperimentalPagerApi::class)
@Composable
fun UpdateDialog(
- modifier: Modifier = Modifier,
- visible: Boolean = false,
- onDismissRequest: () -> Unit = {},
- onConfirm: (String) -> Unit = {},
+ updateViewModel: UpdateViewModel = hiltViewModel(),
) {
val context = LocalContext.current
+ val viewState = updateViewModel.viewState.collectAsStateValue()
+ val downloadState = viewState.downloadFlow.collectAsState(initial = Download.NotYet).value
val scope = rememberCoroutineScope { Dispatchers.IO }
val newVersionNumber = context.dataStore.data
.map { it[DataStoreKeys.NewVersionNumber.key] ?: "" }
@@ -55,17 +61,38 @@ fun UpdateDialog(
.map { it[DataStoreKeys.NewVersionLog.key] ?: "" }
.collectAsState(initial = "")
.value
- val newVersionSize = context.dataStore.data
+ val newVersionSize = " " + context.dataStore.data
.map { it[DataStoreKeys.NewVersionSize.key] ?: 0 }
.map { it / 1024f / 1024f }
.map { if (it > 0f) " ${String.format("%.2f", it)} MB" else "" }
.collectAsState(initial = 0)
.value
+ val settings = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ context.installLatestApk()
+ }
+
+ val launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { result ->
+ if (result) {
+ context.installLatestApk()
+ } else {
+ settings.launch(
+ Intent(
+ Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
+ Uri.parse("package:${context.packageName}"),
+ ),
+ )
+ }
+ }
+
Dialog(
- modifier = modifier.heightIn(max = 400.dp),
- visible = visible,
- onDismissRequest = onDismissRequest,
+ modifier = Modifier.heightIn(max = 400.dp),
+ visible = viewState.updateDialogVisible,
+ onDismissRequest = { updateViewModel.dispatch(UpdateViewAction.Hide) },
icon = {
Icon(
imageVector = Icons.Rounded.Update,
@@ -79,7 +106,7 @@ fun UpdateDialog(
Text(text = stringResource(R.string.change_log))
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = newVersionPublishDate,
+ text = "$newVersionPublishDate$newVersionSize",
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
style = MaterialTheme.typography.bodyMedium,
)
@@ -97,19 +124,47 @@ fun UpdateDialog(
confirmButton = {
TextButton(
onClick = {
+ if (downloadState !is Download.Progress) {
+ updateViewModel.dispatch(
+ UpdateViewAction.DownloadUpdate(
+ url = context.newVersionDownloadUrl,
+ )
+ )
+ }
}
) {
- Text(text = stringResource(R.string.update) + newVersionSize)
+ Text(
+ text = stringResource(R.string.update) + when (downloadState) {
+ is Download.NotYet -> ""
+ is Download.Progress -> " ${downloadState.percent}%"
+ is Download.Finished -> {
+ if (context.packageManager.canRequestPackageInstalls()) {
+ Log.i(
+ "RLog",
+ "Download.Finished: ${downloadState.file.absolutePath}"
+ )
+ context.installLatestApk()
+ } else {
+ launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES)
+ }
+ ""
+ }
+ }
+ )
}
},
dismissButton = {
- TextButton(onClick = {
- scope.launch {
- context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
- onDismissRequest()
+ if (downloadState !is Download.Progress) {
+ TextButton(
+ onClick = {
+ scope.launch {
+ context.dataStore.put(DataStoreKeys.SkipVersionNumber, newVersionNumber)
+ updateViewModel.dispatch(UpdateViewAction.Hide)
+ }
+ }
+ ) {
+ Text(text = stringResource(R.string.skip_this_version))
}
- }) {
- Text(text = stringResource(R.string.skip_this_version))
}
},
)
diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt
index 3593603..80f2892 100644
--- a/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/settings/UpdateViewModel.kt
@@ -3,12 +3,10 @@ package me.ash.reader.ui.page.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.repository.AppRepository
+import me.ash.reader.data.source.Download
import javax.inject.Inject
@HiltViewModel
@@ -26,6 +24,7 @@ class UpdateViewModel @Inject constructor(
action.preProcessor,
action.postProcessor
)
+ is UpdateViewAction.DownloadUpdate -> downloadUpdate(action.url)
}
}
@@ -36,8 +35,10 @@ class UpdateViewModel @Inject constructor(
viewModelScope.launch {
preProcessor()
appRepository.checkUpdate().let {
- changeUpdateDialogVisible(it)
- postProcessor(it)
+ it?.let {
+ changeUpdateDialogVisible(it)
+ postProcessor(it)
+ }
}
}
}
@@ -49,10 +50,26 @@ class UpdateViewModel @Inject constructor(
)
}
}
+
+ private fun downloadUpdate(url: String) {
+ viewModelScope.launch {
+ _viewState.update {
+ it.copy(
+ downloadFlow = flow { emit(Download.Progress(0)) }
+ )
+ }
+ _viewState.update {
+ it.copy(
+ downloadFlow = appRepository.downloadFile(url)
+ )
+ }
+ }
+ }
}
data class UpdateViewState(
val updateDialogVisible: Boolean = false,
+ val downloadFlow: Flow = emptyFlow(),
)
sealed class UpdateViewAction {
@@ -63,4 +80,8 @@ sealed class UpdateViewAction {
val preProcessor: suspend () -> Unit = {},
val postProcessor: suspend (Boolean) -> Unit = {}
) : UpdateViewAction()
+
+ data class DownloadUpdate(
+ val url: String,
+ ) : UpdateViewAction()
}
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 246d0b2..ee1d42f 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -117,4 +117,8 @@
跳过这个版本
正在检查更新…
已是最新版本
+ 检查失败
+ 下载失败
+ 请求速率受限
+ 帮助
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6cd0eb..6fc487b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -117,4 +117,8 @@
Skip This Version
Checking for updates…
This is the latest version
+ Check failure
+ Download failure
+ The request rate is limited
+ Help
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..fb56694
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file