Skip to content

Commit

Permalink
Add download APK file
Browse files Browse the repository at this point in the history
  • Loading branch information
Ashinch committed Apr 24, 2022
1 parent aa65178 commit 840970b
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 91 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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>
11 changes: 7 additions & 4 deletions app/src/main/java/me/ash/reader/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
47 changes: 44 additions & 3 deletions app/src/main/java/me/ash/reader/data/repository/AppRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
25 changes: 25 additions & 0 deletions app/src/main/java/me/ash/reader/ui/ext/ContextExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/me/ash/reader/ui/ext/FileEXT.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
12 changes: 6 additions & 6 deletions app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = "")
Expand Down Expand Up @@ -92,7 +93,9 @@ fun SettingsPage(
contentDescription = stringResource(R.string.close),
)
},
) { updateDialogVisible = true }
) {
updateViewModel.dispatch(UpdateViewAction.Show)
}
}
Banner(
title = stringResource(R.string.in_coding),
Expand Down Expand Up @@ -148,8 +151,5 @@ fun SettingsPage(
}
)

UpdateDialog(
visible = updateDialogVisible,
onDismissRequest = { updateDialogVisible = false },
)
UpdateDialog()
}
Loading

0 comments on commit 840970b

Please sign in to comment.