AppUpdator.kt

This commit is contained in:
sathwikcs
2025-02-06 15:22:37 +05:30
parent 5ade8d4d83
commit 74300239a3

View File

@@ -2,6 +2,7 @@ package `in`.sminnovations.attendanceapp.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -30,7 +31,7 @@ data class UpdateInfo(
class AppUpdater(private val context: Context) {
sealed class UpdaterState {
object Idle : UpdaterState()
data object Idle : UpdaterState()
data class Downloading(val progress: Float) : UpdaterState()
data class Success(val file: File) : UpdaterState()
data class Error(val message: String) : UpdaterState()
@@ -42,86 +43,78 @@ class AppUpdater(private val context: Context) {
fun downloadAndInstall(url: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val file = downloadApk(url) { progress ->
_updateState.value = UpdaterState.Downloading(progress)
//Added for future use case
// Resolve the URL (e.g. for Google Drive links)
val resolvedUrl = resolveDownloadUrl(url)
val file = downloadApk(resolvedUrl) { progress ->
// Update progress on Main thread
CoroutineScope(Dispatchers.Main).launch {
_updateState.value = UpdaterState.Downloading(progress)
}
}
_updateState.value = UpdaterState.Success(file)
installApk(file)
} catch (e: Exception) {
_updateState.value = UpdaterState.Error(e.message ?: "Unknown error occurred")
_updateState.value = UpdaterState.Error(e.message ?: "Unknown error")
}
}
}
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File {
return withContext(Dispatchers.IO) {
private fun resolveDownloadUrl(url: String): String {
// Optional
// If the URL is from Google Drive, extract file id and convert to direct download URL.
return if (url.contains("drive.google.com")) {
val regex = Regex("(?<=/d/)[^/]+")
val fileId = regex.find(url)?.value ?: Uri.parse(url).getQueryParameter("id")
if (!fileId.isNullOrEmpty())
"https://drive.google.com/uc?export=download&id=$fileId"
else url
} else {
url
}
}
private suspend fun downloadApk(url: String, onProgress: (Float) -> Unit): File =
withContext(Dispatchers.IO) {
val client = OkHttpClient.Builder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
// Verify content type
val contentType = originalResponse.header("Content-Type")
if (contentType?.contains("application/vnd.android.package-archive") != true &&
contentType?.contains("application/octet-stream") != true) {
throw IOException("Invalid content type: $contentType")
}
val contentLength = originalResponse.body?.contentLength() ?: -1L
originalResponse.newBuilder()
.body(originalResponse.body?.let { body ->
ProgressResponseBody(body) { progress ->
CoroutineScope(Dispatchers.Main).launch {
onProgress(progress)
}
onProgress(progress)
}
})
.build()
}
.build()
val request = Request.Builder()
.url(url)
.build()
val request = Request.Builder().url(url).build()
val file = File(context.cacheDir, "update.apk")
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Failed to download update")
if (!response.isSuccessful) throw IOException("Failed to download APK")
response.body?.let { body ->
file.outputStream().use { fileOut ->
body.byteStream().use { bodyIn ->
bodyIn.copyTo(fileOut)
}
body.byteStream().use { it.copyTo(fileOut) }
}
} ?: throw IOException("Empty response")
}
file
}
}
private fun installApk(file: File) {
try {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
file
)
// Verify the file is actually an APK
if (file.length() == 0L) {
throw IOException("Downloaded file is empty")
}
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
if (file.length() == 0L) throw IOException("Downloaded file is empty")
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
// Verify that there's an app to handle the install intent
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
throw IOException("No app found to handle APK installation")
throw IOException("No app found to handle installation")
}
} catch (e: Exception) {
_updateState.value = UpdaterState.Error("Installation failed: ${e.message}")
@@ -136,28 +129,19 @@ private class ProgressResponseBody(
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}
override fun contentType() = responseBody.contentType()
override fun contentLength() = responseBody.contentLength()
override fun source(): BufferedSource = bufferedSource
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
private fun source(source: Source): Source =
object : ForwardingSource(source) {
var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
val progress = if (contentLength() > 0) {
totalBytesRead.toFloat() / contentLength()
} else {
-1f
}
val progress = if (contentLength() > 0) totalBytesRead.toFloat() / contentLength() else -1f
onProgressUpdate(progress)
return bytesRead
}
}
}
}