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