Add download APK file
This commit is contained in:
parent
aa6517818c
commit
840970bfe2
|
@ -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 {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package="me.ash.reader">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
@ -24,10 +25,21 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove"></provider>
|
||||
tools:node="remove" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -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 =
|
||||
|
|
|
@ -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<Download> =
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<LatestRelease>
|
||||
|
||||
@GET
|
||||
@Streaming
|
||||
suspend fun downloadFile(@Url url: String): ResponseBody
|
||||
|
||||
companion object {
|
||||
private var instance: AppNetworkDataSource? = null
|
||||
|
@ -24,4 +43,53 @@ interface AppNetworkDataSource {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ResponseBody.downloadToFileWithProgress(saveFile: File): Flow<Download> =
|
||||
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()
|
|
@ -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) {
|
||||
|
|
38
app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
Normal file
38
app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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<Download> = 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()
|
||||
}
|
|
@ -117,4 +117,8 @@
|
|||
<string name="skip_this_version">跳过这个版本</string>
|
||||
<string name="checking_updates">正在检查更新…</string>
|
||||
<string name="is_latest_version">已是最新版本</string>
|
||||
<string name="check_failure">检查失败</string>
|
||||
<string name="download_failure">下载失败</string>
|
||||
<string name="rate_limit">请求速率受限</string>
|
||||
<string name="help">帮助</string>
|
||||
</resources>
|
|
@ -117,4 +117,8 @@
|
|||
<string name="skip_this_version">Skip This Version</string>
|
||||
<string name="checking_updates">Checking for updates…</string>
|
||||
<string name="is_latest_version">This is the latest version</string>
|
||||
<string name="check_failure">Check failure</string>
|
||||
<string name="download_failure">Download failure</string>
|
||||
<string name="rate_limit">The request rate is limited</string>
|
||||
<string name="help">Help</string>
|
||||
</resources>
|
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="."/>
|
||||
</paths>
|
Loading…
Reference in New Issue
Block a user