AppUpdator.kt
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user